`weak var` in `Sendable class`

So, I was making a class, right:

final class C: Sendable {}

final class X: Sendable {
    weak var c: C?
    init(c: C) {
        self.c = c
    }
}

But the compiler goes

Stored property 'a' of 'Sendable'-conforming class 'X' is mutable

Which makes sense! It is mutable! But then I had a horrible thought:

final class C: Sendable {}

struct Weak<T: AnyObject> {
    weak var object: T?
}
extension Weak: Sendable where T: Sendable {}

final class X: Sendable {
    let c: Weak<C>
    init(c: C) {
        self.c = Weak(object: c)
    }
}

And the compiler is fine with this. Which led me to question whether the weak object here can actually become nil, and it can:

var c: C? = C()
let x = X(c: c!)
c = nil
print(x.c.object as Any)

Now, I don't think this is actually unsafe — refcounting is atomic; I don't think there's any data races here. So I think I should file a compiler bug or make an evolution proposal. But what is the right direction?

  1. weak var should be allowed in Sendable class, because it effectively is, anyway (unsure if this is true — is nilling when the last reference drops actually safer than reassigning in some way?)
  2. weak let should be allowed, in the kind of vein of @_staticExclusiveOnly (Atomic, Mutex) — things that can be safely modified from other tasks/threads (weak var would still exist, the difference being whether manual reassignment is allowed)
  3. weak should be deprecated, replaced by a stdlib @_staticExclusiveOnly Weak struct — it's a pretty weird point in the language syntax already and there are plenty of cases where you need Weak already (eg. collections)
  4. weak should be disallowed in Sendable struct/enum
  5. something else?!

This is not true -- the reference can be changed besides becoming nil.

let c1 = C()
let c2 = C()
let x = X(c: c1)
x.c = c2

When you wrap it in a struct, you can change it from var to let, preventing this kind of mutation.

1 Like

Another limitation of struct Weak:

protocol P: AnyObject, Sendable {}
struct Weak<T: AnyObject> {
    weak var wrapped: T?
}

weak var p: (any P)? // allowed
let p: Weak<any P> // 'Weak' requires that 'any P' be a class type

Which… I know why it happens (implementors of P are required to be class types, the existential any P is not), but it's pretty unhelpful, and I think it should work, technically…