Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
Don't mark prop set source as stolen in SetAttr
  • Loading branch information
p-sawicki committed Mar 24, 2026
commit 52d4a5899c1160c80013a20a4fa27ffe87e9fe0a
5 changes: 0 additions & 5 deletions mypyc/codegen/emitfunc.py
Original file line number Diff line number Diff line change
Expand Up @@ -507,11 +507,6 @@ def visit_set_attr(self, op: SetAttr) -> None:
op.attr,
)
)
# The property setter method increfs the passed value (src) so we need to decref it here
# to avoid leaking.
src_rtype = op.src.type
if src_rtype.is_refcounted:
self.emitter.emit_dec_ref(src, src_rtype)
else:
# ...and struct access for normal attributes.
attr_expr = self.get_attr_expr(obj, op, decl_cl)
Expand Down
13 changes: 9 additions & 4 deletions mypyc/ir/ops.py
Original file line number Diff line number Diff line change
Expand Up @@ -909,10 +909,7 @@ def accept(self, visitor: OpVisitor[T]) -> T:

@final
class SetAttr(RegisterOp):
"""obj.attr = src (for a native object)

Steals the reference to src.
"""
"""obj.attr = src (for a native object)"""

error_kind = ERR_FALSE

Expand All @@ -928,6 +925,10 @@ def __init__(self, obj: Value, attr: str, src: Value, line: int) -> None:
# and we don't use a setter
self.is_init = False

cl = self.class_type.class_ir
# If True, this op represents calling a property setter.
self.is_propset = cl.get_method(self.attr) is not None

def mark_as_initializer(self) -> None:
self.is_init = True
self.error_kind = ERR_NEVER
Expand All @@ -940,6 +941,10 @@ def set_sources(self, new: list[Value]) -> None:
self.obj, self.src = new

def stolen(self) -> list[Value]:
# The property setter method increfs the passed value so don't treat it as a steal
# to avoid leaking.
if self.is_propset:
return []
return [self.src]

def accept(self, visitor: OpVisitor[T]) -> T:
Expand Down
128 changes: 128 additions & 0 deletions mypyc/test-data/refcount.test
Original file line number Diff line number Diff line change
Expand Up @@ -1931,3 +1931,131 @@ L0:
r10 = unborrow r8
x = r10
return x

[case testPropertySetterCallWithRefcountedObject]
class Foo:
def __init__(self) -> None:
self.attr = "unmodified"

class A:
def __init__(self) -> None:
self._foo = Foo()

@property
def foo(self) -> Foo:
return self._foo

@foo.setter
def foo(self, val : Foo) -> None:
self._foo = val

def f(a: A):
a.foo = Foo()
[out]
def Foo.__init__(self):
self :: __main__.Foo
r0 :: str
L0:
r0 = 'unmodified'
inc_ref r0
self.attr = r0
return 1
def A.__init__(self):
self :: __main__.A
r0 :: __main__.Foo
L0:
r0 = Foo()
self._foo = r0
return 1
def A.foo(self):
self :: __main__.A
r0 :: __main__.Foo
L0:
r0 = self._foo
return r0
def A.__mypyc_setter__foo(self, val):
self :: __main__.A
val :: __main__.Foo
r0 :: bool
L0:
inc_ref val
self._foo = val; r0 = is_error
return 1
def f(a):
a :: __main__.A
r0 :: __main__.Foo
r1 :: bool
r2 :: object
L0:
r0 = Foo()
a.foo = r0; r1 = is_error
dec_ref r0
r2 = box(None, 1)
inc_ref r2
return r2

[case testPropertySetterCallWithNonRefcountedObject]
from mypy_extensions import i64

class A:
def __init__(self) -> None:
self._foo = i64(0)

@property
def foo(self) -> i64:
return self._foo

@foo.setter
def foo(self, val : i64) -> None:
self._foo = val

def f(a: A, i: int):
a.foo = i64(i)
[out]
def A.__init__(self):
self :: __main__.A
L0:
self._foo = 0
return 1
def A.foo(self):
self :: __main__.A
r0 :: i64
L0:
r0 = self._foo
return r0
def A.__mypyc_setter__foo(self, val):
self :: __main__.A
val :: i64
r0 :: bool
L0:
self._foo = val; r0 = is_error
return 1
def f(a, i):
a :: __main__.A
i :: int
r0 :: native_int
r1 :: bit
r2, r3 :: i64
r4 :: ptr
r5 :: c_ptr
r6 :: i64
r7 :: bool
r8 :: object
L0:
r0 = i & 1
r1 = r0 == 0
if r1 goto L1 else goto L2 :: bool
L1:
r2 = i >> 1
r3 = r2
goto L3
L2:
r4 = i ^ 1
r5 = r4
r6 = CPyLong_AsInt64(r5)
r3 = r6
L3:
a.foo = r3; r7 = is_error
r8 = box(None, 1)
inc_ref r8
return r8
16 changes: 0 additions & 16 deletions mypyc/test-data/run-classes.test
Original file line number Diff line number Diff line change
Expand Up @@ -2164,21 +2164,6 @@ import gc

from native import A, B, C, D, E, F, Foo, G

def test_leaks() -> None:
a = A()

gc.collect()
before = gc.get_objects()
for _ in range(100):
a.foo = Foo()
gc.collect()
after = gc.get_objects()

diff = len(after) - len(before)
# Small difference is expected, if the property setter call was leaking objects
# we would get a difference similar to the number of iterations above.
assert diff <= 2, diff

def set_foo(foo: Foo) -> None:
a = A()
a.foo = foo
Expand Down Expand Up @@ -2227,7 +2212,6 @@ g = G(4)
g.x = 20
assert g.x == 20

test_leaks()
test_use_after_passing_to_prop_setter()

[file driver.py]
Expand Down
Loading