diff --git a/mypyc/irbuild/builder.py b/mypyc/irbuild/builder.py index 39e2c6aa5181f..56a3f944fe77f 100644 --- a/mypyc/irbuild/builder.py +++ b/mypyc/irbuild/builder.py @@ -1565,6 +1565,22 @@ def is_native_attr_ref(self, expr: MemberExpr) -> bool: and any(expr.name in ir.attributes for ir in obj_rtype.class_ir.mro) ) + def is_final_native_attr_ref(self, expr: MemberExpr) -> bool: + """Is expr a direct reference to a Final native (struct) attribute of an instance? + + A Final attribute is read-only at runtime (it has no setter), so it can never be + reassigned after construction. This makes it safe to borrow even on free-threaded + builds, since no concurrent store can invalidate the borrowed reference. + """ + obj_rtype = self.node_type(expr.expr) + if not (isinstance(obj_rtype, RInstance) and obj_rtype.class_ir.is_ext_class): + return False + # Find the class that defines the attribute and check whether it's Final there. + for ir in obj_rtype.class_ir.mro: + if expr.name in ir.attributes: + return expr.name in ir.final_attributes + return False + def mark_block_unreachable(self) -> None: """Mark statements in the innermost block being processed as unreachable. diff --git a/mypyc/irbuild/expression.py b/mypyc/irbuild/expression.py index 21cb2f8df1b8f..b99e8161c08c5 100644 --- a/mypyc/irbuild/expression.py +++ b/mypyc/irbuild/expression.py @@ -272,9 +272,16 @@ def transform_member_expr(builder: IRBuilder, expr: MemberExpr) -> Value: if isinstance(expr.node, MypyFile) and expr.node.fullname in builder.imports: return builder.load_module(expr.node.fullname) - can_borrow = builder.is_native_attr_ref(expr) - obj = builder.accept(expr.expr, can_borrow=can_borrow) rtype = builder.node_type(expr) + # Borrowing a native attribute read is unsafe on free-threaded builds, since another + # thread could concurrently reassign the attribute and free the old value. 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. + can_borrow = builder.is_native_attr_ref(expr) and ( + not IS_FREE_THREADED or isinstance(rtype, RVec) or builder.is_final_native_attr_ref(expr) + ) + obj = builder.accept(expr.expr, can_borrow=can_borrow) if ( is_object_rprimitive(obj.type) diff --git a/mypyc/test-data/exceptions.test b/mypyc/test-data/exceptions.test index 1fea2a1eb9203..b8d8b4727363c 100644 --- a/mypyc/test-data/exceptions.test +++ b/mypyc/test-data/exceptions.test @@ -553,7 +553,7 @@ L3: r3 = :: i64 return r3 -[case testExceptionWithNativeAttributeGetAndSet] +[case testExceptionWithNativeAttributeGetAndSet_withgil] class C: def __init__(self, x: int) -> None: self.x = x @@ -578,6 +578,32 @@ L0: c.x = r1 return 1 +[case testExceptionWithNativeAttributeGetAndSet_nogil] +class C: + def __init__(self, x: int) -> None: + self.x = x + +def foo(c: C, x: int) -> None: + c.x = x - c.x +[out] +def C.__init__(self, x): + self :: __main__.C + x :: int +L0: + inc_ref x :: int + self.x = x + return 1 +def foo(c, x): + c :: __main__.C + x, r0, r1 :: int + r2 :: bool +L0: + r0 = c.x + r1 = CPyTagged_Subtract(x, r0) + dec_ref r0 :: int + c.x = r1 + return 1 + [case testExceptionWithOverlappingFloatErrorValue] def f() -> float: return 0.0 diff --git a/mypyc/test-data/irbuild-basic.test b/mypyc/test-data/irbuild-basic.test index e0c86e4cfb93f..c6c231f0386be 100644 --- a/mypyc/test-data/irbuild-basic.test +++ b/mypyc/test-data/irbuild-basic.test @@ -2063,7 +2063,7 @@ L7: L8: return r10 -[case testProperty] +[case testProperty_withgil] class PropertyHolder: @property def value(self) -> int: diff --git a/mypyc/test-data/irbuild-classes.test b/mypyc/test-data/irbuild-classes.test index 0310d2f69d444..66caf0772ec40 100644 --- a/mypyc/test-data/irbuild-classes.test +++ b/mypyc/test-data/irbuild-classes.test @@ -26,7 +26,7 @@ L0: a.x = 2; r0 = is_error return 1 -[case testUserClassInList] +[case testUserClassInList_withgil] class C: x: int @@ -103,7 +103,7 @@ L0: r0 = a.n return r0 -[case testOptionalMember] +[case testOptionalMember_withgil] from typing import Optional class Node: next: Optional[Node] @@ -1239,7 +1239,7 @@ L0: __mypyc_self__.s = r0 return 1 -[case testBorrowAttribute] +[case testBorrowAttribute_withgil] def f(d: D) -> int: return d.c.x @@ -1258,6 +1258,24 @@ L0: keep_alive d return r1 +[case testCannotBorrowAttribute_nogil] +def f(d: D) -> int: + return d.c.x + +class C: + x: int +class D: + c: C +[out] +def f(d): + d :: __main__.D + r0 :: __main__.C + r1 :: int +L0: + r0 = d.c + r1 = r0.x + return r1 + [case testNoBorrowOverPropertyAccess] class C: d: D diff --git a/mypyc/test-data/irbuild-generics.test b/mypyc/test-data/irbuild-generics.test index 9ec29182e89b6..0d4dad4f97e23 100644 --- a/mypyc/test-data/irbuild-generics.test +++ b/mypyc/test-data/irbuild-generics.test @@ -40,7 +40,7 @@ L0: y = r3 return 1 -[case testGenericAttrAndTypeApplication] +[case testGenericAttrAndTypeApplication_withgil] from typing import TypeVar, Generic T = TypeVar('T') class C(Generic[T]): diff --git a/mypyc/test-data/irbuild-glue-methods.test b/mypyc/test-data/irbuild-glue-methods.test index 87b3bf7fee588..bdbdb7e2afa68 100644 --- a/mypyc/test-data/irbuild-glue-methods.test +++ b/mypyc/test-data/irbuild-glue-methods.test @@ -91,7 +91,7 @@ L0: r0 = x.foo(y) return r0 -[case testPropertyDerivedGen] +[case testPropertyDerivedGen_withgil] from typing import Callable class BaseProperty: @property diff --git a/mypyc/test-data/irbuild-i64.test b/mypyc/test-data/irbuild-i64.test index 44157f5a820a0..4cea7894052aa 100644 --- a/mypyc/test-data/irbuild-i64.test +++ b/mypyc/test-data/irbuild-i64.test @@ -1032,7 +1032,7 @@ L4: y = r3 return y -[case testBorrowOverI64Arithmetic] +[case testBorrowOverI64Arithmetic_withgil] from mypy_extensions import i64 def add_simple(c: C) -> i64: @@ -1084,7 +1084,7 @@ L0: keep_alive d, d return r4 -[case testBorrowOverI64Bitwise] +[case testBorrowOverI64Bitwise_withgil] from mypy_extensions import i64 def bitwise_simple(c: C) -> i64: @@ -2070,7 +2070,7 @@ L4: r6 = CPyFloat_FromTagged(r3) return r6 -[case testI64IsinstanceNarrowing] +[case testI64IsinstanceNarrowing_withgil] from typing import Union from mypy_extensions import i64 diff --git a/mypyc/test-data/irbuild-isinstance.test b/mypyc/test-data/irbuild-isinstance.test index 36a9300350bd3..216a52ff2a25c 100644 --- a/mypyc/test-data/irbuild-isinstance.test +++ b/mypyc/test-data/irbuild-isinstance.test @@ -48,7 +48,7 @@ L2: L3: return r1 -[case testBorrowSpecialCaseWithIsinstance] +[case testBorrowSpecialCaseWithIsinstance_withgil] class C: s: str diff --git a/mypyc/test-data/irbuild-optional.test b/mypyc/test-data/irbuild-optional.test index fbf7cb148b089..252f9489dc57f 100644 --- a/mypyc/test-data/irbuild-optional.test +++ b/mypyc/test-data/irbuild-optional.test @@ -237,7 +237,7 @@ L3: L4: return 1 -[case testUnionType] +[case testUnionType_withgil] from typing import Union class A: diff --git a/mypyc/test-data/irbuild-vec-t.test b/mypyc/test-data/irbuild-vec-t.test index ee48d81fc29c8..ce162bd0806c7 100644 --- a/mypyc/test-data/irbuild-vec-t.test +++ b/mypyc/test-data/irbuild-vec-t.test @@ -512,15 +512,25 @@ L0: return r2 [case testVecTBorrowGetItem_64bit] +from typing import Final + from librt.vecs import vec from mypy_extensions import i64 class A: - x: str + def __init__(self, x: str) -> None: + # Final to allow borrowing on free-threaded builds + self.x: Final = x def f(v: vec[A], n: i64) -> int: return len(v[n].x) [out] +def A.__init__(self, x): + self :: __main__.A + x :: str +L0: + self.x = x + return 1 def f(v, n): v :: vec[__main__.A] n :: i64 diff --git a/mypyc/test-data/opt-copy-propagation.test b/mypyc/test-data/opt-copy-propagation.test index 49b80f4385fc4..9fa5edb579fac 100644 --- a/mypyc/test-data/opt-copy-propagation.test +++ b/mypyc/test-data/opt-copy-propagation.test @@ -185,7 +185,7 @@ L1: L2: return 2 -[case testIRTransformRegisterOps1] +[case testIRTransformRegisterOps1_withgil] from __future__ import annotations from typing import cast diff --git a/mypyc/test-data/refcount.test b/mypyc/test-data/refcount.test index 7d88cb7d11623..7c7134dcfbfaa 100644 --- a/mypyc/test-data/refcount.test +++ b/mypyc/test-data/refcount.test @@ -651,7 +651,7 @@ L0: r4 = (r2, r3) return r4 -[case testDecomposeTuple] +[case testDecomposeTuple_withgil] from typing import Tuple class C: @@ -970,7 +970,7 @@ L0: dec_ref r4 return r5 -[case testBorrowAttribute] +[case testBorrowAttribute_withgil] def g() -> int: d = D() return d.c.x @@ -1003,7 +1003,99 @@ L0: r1 = r0.x return r1 -[case testBorrowAttributeTwice] +[case testBorrowAttribute_nogil] +from typing import Final + +def g() -> int: + d = D(C(1)) + return d.c.x + +def f(d: D) -> int: + return d.c.x + +class C: + def __init__(self, x: int) -> None: + self.x: Final = x +class D: + def __init__(self, c: C) -> None: + self.c: Final = c +[out] +def g(): + r0 :: __main__.C + r1, d :: __main__.D + r2 :: __main__.C + r3 :: int +L0: + r0 = C(2) + r1 = D(r0) + dec_ref r0 + d = r1 + r2 = borrow d.c + r3 = r2.x + dec_ref d + return r3 +def f(d): + d :: __main__.D + r0 :: __main__.C + r1 :: int +L0: + r0 = borrow d.c + r1 = r0.x + return r1 +def C.__init__(self, x): + self :: __main__.C + x :: int +L0: + inc_ref x :: int + self.x = x + return 1 +def D.__init__(self, c): + self :: __main__.D + c :: __main__.C +L0: + inc_ref c + self.c = c + return 1 + +[case testBorrowInheritedFinalAttribute_nogil] +from typing import Final + +def f(d: D) -> int: + return d.c.x + +class B: + def __init__(self, x: int) -> None: + self.x: Final = x +class C(B): + pass +class D: + def __init__(self, c: C) -> None: + self.c: Final = c +[out] +def f(d): + d :: __main__.D + r0 :: __main__.C + r1 :: int +L0: + r0 = borrow d.c + r1 = r0.x + return r1 +def B.__init__(self, x): + self :: __main__.B + x :: int +L0: + inc_ref x :: int + self.x = x + return 1 +def D.__init__(self, c): + self :: __main__.D + c :: __main__.C +L0: + inc_ref c + self.c = c + return 1 + +[case testBorrowAttributeTwice_withgil] def f(e: E) -> int: return e.d.c.x @@ -1025,7 +1117,7 @@ L0: r2 = r1.x return r2 -[case testBorrowAttributeIsNone] +[case testBorrowAttributeIsNone_withgil] from typing import Optional def f(c: C) -> bool: @@ -1058,7 +1150,7 @@ L0: r2 = r0 == r1 return r2 -[case testBorrowAttributeNarrowOptional] +[case testBorrowAttributeNarrowOptional_withgil] from typing import Optional def f(c: C) -> bool: @@ -1093,7 +1185,7 @@ L1: L2: return 0 -[case testBorrowLenArgument] +[case testBorrowLenArgument_withgil] from typing import List def f(x: C) -> int: @@ -1113,7 +1205,7 @@ L0: r2 = r1 << 1 return r2 -[case testBorrowIsinstanceArgument] +[case testBorrowIsinstanceArgument_withgil] from typing import List def f(x: C) -> bool: @@ -1152,7 +1244,7 @@ L1: L2: return 1 -[case testBorrowListGetItem1] +[case testBorrowListGetItem1_withgil] from typing import List def literal_index(x: C) -> str: @@ -1200,6 +1292,57 @@ L0: r2 = cast(str, r1) return r2 +[case testBorrowListGetItem1_nogil] +from typing import List + +def literal_index(x: C) -> str: + return x.a[0] + +def negative_index(x: C) -> str: + return x.a[-1] + +def lvar_index(x: C, n: int) -> str: + return x.a[n] + +class C: + a: List[str] + +[out] +def literal_index(x): + x :: __main__.C + r0 :: list + r1 :: object + r2 :: str +L0: + r0 = x.a + r1 = CPyList_GetItemShort(r0, 0) + dec_ref r0 + r2 = cast(str, r1) + return r2 +def negative_index(x): + x :: __main__.C + r0 :: list + r1 :: object + r2 :: str +L0: + r0 = x.a + r1 = CPyList_GetItemShort(r0, -2) + dec_ref r0 + r2 = cast(str, r1) + return r2 +def lvar_index(x, n): + x :: __main__.C + n :: int + r0 :: list + r1 :: object + r2 :: str +L0: + r0 = x.a + r1 = CPyList_GetItem(r0, n) + dec_ref r0 + r2 = cast(str, r1) + return r2 + [case testBorrowListGetItem2_withgil] from typing import List @@ -1273,9 +1416,11 @@ def attr_before_index(x): r2 :: object r3 :: str L0: - r0 = borrow x.a - r1 = borrow x.n + r0 = x.a + r1 = x.n r2 = CPyList_GetItem(r0, r1) + dec_ref r0 + dec_ref r1 :: int r3 = cast(str, r2) return r3 def attr_after_index(a, i): @@ -1361,7 +1506,7 @@ L0: dec_ref a return r5 -[case testBorrowSetAttrObject] +[case testBorrowSetAttrObject_withgil] from typing import Optional def f(x: Optional[C]) -> None: @@ -1401,7 +1546,7 @@ L0: r0.b = 0; r1 = is_error return 1 -[case testBorrowIntEquality] +[case testBorrowIntEquality_withgil] def add(c: C) -> bool: return c.x == c.y @@ -1419,7 +1564,7 @@ L0: r2 = int_eq r0, r1 return r2 -[case testBorrowIntLessThan] +[case testBorrowIntLessThan_withgil] def add(c: C) -> bool: return c.x < c.y @@ -1437,7 +1582,7 @@ L0: r2 = int_lt r0, r1 return r2 -[case testBorrowIntCompareFinal] +[case testBorrowIntCompareFinal_withgil] from typing import Final X: Final = 10 @@ -1457,7 +1602,7 @@ L0: r1 = int_eq r0, 20 return r1 -[case testBorrowIntArithmetic] +[case testBorrowIntArithmetic_withgil] def add(c: C) -> int: return c.x + c.y @@ -1485,7 +1630,39 @@ L0: r2 = CPyTagged_Subtract(r0, r1) return r2 -[case testBorrowIntComparisonInIf] +[case testBorrowIntArithmetic_nogil] +def add(c: C) -> int: + return c.x + c.y + +def sub(c: C) -> int: + return c.x - c.y + +class C: + x: int + y: int +[out] +def add(c): + c :: __main__.C + r0, r1, r2 :: int +L0: + r0 = c.x + r1 = c.y + r2 = CPyTagged_Add(r0, r1) + dec_ref r0 :: int + dec_ref r1 :: int + return r2 +def sub(c): + c :: __main__.C + r0, r1, r2 :: int +L0: + r0 = c.x + r1 = c.y + r2 = CPyTagged_Subtract(r0, r1) + dec_ref r0 :: int + dec_ref r1 :: int + return r2 + +[case testBorrowIntComparisonInIf_withgil] def add(c: C, n: int) -> bool: if c.x == c.y: return True @@ -1509,7 +1686,7 @@ L1: L2: return 0 -[case testBorrowIntInPlaceOp] +[case testBorrowIntInPlaceOp_withgil] def add(c: C, n: int) -> None: c.x += n diff --git a/mypyc/test/test_optimizations.py b/mypyc/test/test_optimizations.py index 6ca53b134ba91..c6a40d08e2e67 100644 --- a/mypyc/test/test_optimizations.py +++ b/mypyc/test/test_optimizations.py @@ -7,7 +7,7 @@ from mypy.errors import CompileError from mypy.test.config import test_temp_dir from mypy.test.data import DataDrivenTestCase -from mypyc.common import TOP_LEVEL_NAME +from mypyc.common import IS_FREE_THREADED, TOP_LEVEL_NAME from mypyc.ir.func_ir import FuncIR from mypyc.ir.pprint import format_func from mypyc.options import CompilerOptions @@ -33,6 +33,12 @@ class OptimizationSuite(MypycDataSuite): base_path = test_temp_dir def run_case(self, testcase: DataDrivenTestCase) -> None: + if "_withgil" in testcase.name and IS_FREE_THREADED: + # Test case should only run on a non-free-threaded build. + return + if "_nogil" in testcase.name and not IS_FREE_THREADED: + # Test case should only run on a free-threaded build. + return with use_custom_builtins(os.path.join(self.data_prefix, ICODE_GEN_BUILTINS), testcase): expected_output = remove_comment_lines(testcase.output) try: