Skip to content

Commit d1173b8

Browse files
authored
[mypyc] Fix handling of failed cast in non-native attr setter (#21877)
Fix order of increfs and decrefs in setter. I used coding agent assist.
1 parent 7f88c95 commit d1173b8

2 files changed

Lines changed: 81 additions & 7 deletions

File tree

mypyc/codegen/emitclass.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1334,13 +1334,14 @@ def generate_setter(cl: ClassIR, attr: str, rtype: RType, emitter: Emitter) -> N
13341334
# values is benign.
13351335
always_defined = cl.is_always_defined(attr) and not rtype.is_refcounted
13361336

1337-
if rtype.is_refcounted:
1338-
attr_expr = f"self->{attr_field}"
1339-
if not always_defined:
1340-
emitter.emit_undefined_attr_check(rtype, attr_expr, "!=", "self", attr, cl)
1341-
emitter.emit_dec_ref(f"self->{attr_field}", rtype)
1342-
if not always_defined:
1343-
emitter.emit_line("}")
1337+
def emit_decref_old_value() -> None:
1338+
if rtype.is_refcounted:
1339+
attr_expr = f"self->{attr_field}"
1340+
if not always_defined:
1341+
emitter.emit_undefined_attr_check(rtype, attr_expr, "!=", "self", attr, cl)
1342+
emitter.emit_dec_ref(attr_expr, rtype)
1343+
if not always_defined:
1344+
emitter.emit_line("}")
13441345

13451346
if deletable:
13461347
emitter.emit_line("if (value != NULL) {")
@@ -1360,13 +1361,17 @@ def generate_setter(cl: ClassIR, attr: str, rtype: RType, emitter: Emitter) -> N
13601361
else:
13611362
emitter.emit_cast("value", "tmp", rtype, declare_dest=True)
13621363
emitter.emit_lines("if (!tmp)", " return -1;")
1364+
# Take ownership of the incoming value before releasing the old one. In
1365+
# particular, a failed cast must leave the attribute and its reference intact.
13631366
emitter.emit_inc_ref("tmp", rtype)
1367+
emit_decref_old_value()
13641368
emitter.emit_line(f"self->{attr_field} = tmp;")
13651369
if rtype.error_overlap and not always_defined:
13661370
emitter.emit_attr_bitmap_set("tmp", "self", rtype, cl, attr)
13671371

13681372
if deletable:
13691373
emitter.emit_line("} else {")
1374+
emit_decref_old_value()
13701375
emitter.set_undefined_value(f"self->{attr_field}", rtype)
13711376
if rtype.error_overlap:
13721377
emitter.emit_attr_bitmap_clear("self", rtype, cl, attr)

mypyc/test-data/run-classes.test

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6219,6 +6219,75 @@ o.v = BIG
62196219
o.v = BIG
62206220
assert sys.getrefcount(BIG) == base, "reassignment leaked refs"
62216221

6222+
[case testNativeAttrSetterTypeErrorPreservesValue]
6223+
import sys
6224+
from typing import Any
6225+
from testutil import assertRaises
6226+
6227+
class Fields:
6228+
def __init__(self, items: list[int], number: int, pair: tuple[int, int]) -> None:
6229+
self.items = items
6230+
self.number = number
6231+
self.pair = pair
6232+
6233+
class DeletableFields:
6234+
__deletable__ = ("pair",)
6235+
6236+
def __init__(self, pair: tuple[int, int]) -> None:
6237+
self.pair = pair
6238+
6239+
def test_type_error_preserves_value() -> None:
6240+
getrefcount: Any = getattr(sys, "getrefcount")
6241+
items = [1]
6242+
shift = 70
6243+
number = 1 << shift
6244+
pair_number = 1 << (shift + 1)
6245+
pair = (pair_number, pair_number)
6246+
fields = Fields(items, number, pair)
6247+
dynamic_fields: Any = fields
6248+
items_refcount = getrefcount(items)
6249+
number_refcount = getrefcount(number)
6250+
pair_number_refcount = getrefcount(pair_number)
6251+
6252+
with assertRaises(AttributeError):
6253+
del dynamic_fields.items
6254+
# Do not make successful pointer assignments before these checks. Free-threaded
6255+
# builds reclaim the replaced value using a QSBR-delayed decref.
6256+
with assertRaises(TypeError):
6257+
dynamic_fields.items = object()
6258+
with assertRaises(TypeError):
6259+
dynamic_fields.number = object()
6260+
with assertRaises(TypeError):
6261+
dynamic_fields.pair = object()
6262+
6263+
assert getrefcount(items) == items_refcount
6264+
assert getrefcount(number) == number_refcount
6265+
assert getrefcount(pair_number) == pair_number_refcount
6266+
assert fields.items is items
6267+
assert fields.number is number
6268+
assert fields.pair == pair
6269+
assert fields.pair[0] is pair_number
6270+
6271+
def test_deletion_releases_value() -> None:
6272+
getrefcount: Any = getattr(sys, "getrefcount")
6273+
shift = 70
6274+
number = 1 << shift
6275+
# Single-word reference fields use delayed decrefs on free-threaded builds. An
6276+
# unboxed tuple instead exercises the synchronous deletion path changed by the fix.
6277+
pair = (number, number)
6278+
fields = DeletableFields(pair)
6279+
dynamic_fields: Any = fields
6280+
stored_refcount = getrefcount(number)
6281+
6282+
del dynamic_fields.pair
6283+
6284+
after = getrefcount(number)
6285+
assert after == stored_refcount - 2, (stored_refcount, after)
6286+
# Keep the compiled tuple local live across both refcount measurements.
6287+
assert pair[0] is number
6288+
with assertRaises(AttributeError):
6289+
fields.pair
6290+
62226291
[case testBorrowedFinalAttributeInLambdaAndNestedFunction]
62236292
from typing import Final, Callable
62246293

0 commit comments

Comments
 (0)