Strange "cannot assign to property" error

I do not understand why I am getting an error here:

protocol ValueProtocol {}
extension Int: ValueProtocol {}

protocol P: AnyObject {
    associatedtype Value: ValueProtocol
    var a: Int { get set }
    var b: Value { get set }
}

class C: P {
    var a: Int = 0
    var b: Int = 0
    
    func foo(p: any P) {
        self.a = 0  // ✅
        self.b = 0  // ✅
        
        let copy = self
        copy.a = 0  // ✅
        copy.b = 0  // ✅
        
        p.a = 0     // ✅
        p.b = 0     // ❌ cannot assign to property: 'p' is a 'let' constant
    }
}

The wording of the error is particularly strange: copy is also let and so is p on the line just above, besides P is restricted to be of a class type for which let should not matter.

FWIW, same error with generic parameter: func foo<T: P>(p: T) { ... }

3 Likes

Same happens if you spell let copy = self as any P FWIW.

I think the error itself is sound, the diagnostic is just wrong. In your case, P.Value is not any ValueProtocol, it's some concrete type for any specific implementor of P. At the callsite, the compiler doesn't know whether it's Int in your case, so it can't allow you to assign it.

It would work if the protocol declared the property as var b: any ValueProtocol { get set } instead.

To see the difference, notice that this other option also works — when you get to specify the concrete type of Value using some P<Int>:

protocol ValueProtocol {}
extension Int: ValueProtocol {}

protocol P<Value>: AnyObject {
    associatedtype Value: ValueProtocol
    var a: Int { get set }
    var b: Value { get set }
}

func foo(p: some P<Int>) {
    p.b = 1
}
6 Likes

I concur with @nkbelov's assessment – one additional option, if you don't want to go the primary associated type route, is to introduce additional generic constraints so the compiler knows the type being assigned to b is actually P.Value:

func foo<T: P>(p: T) where P.Value == Int {
  p.b = 0 // ✅
}

IMO still worth filing a bug for the bad diagnostic though since the wording we end up with makes no sense here really.

4 Likes

The wording changes for me; it's only broken with any. (This is a reduction of your code, but the same change happens with your exact example.)

protocol P {
  associatedtype V
  var v: V { get nonmutating set }
}

func f(p: any P) {
  p.v = 0 // ❌ Cannot assign to property: 'p' is a 'let' constant
}

func f(p: some P) {
  p.v = 0 // ❌ Cannot assign value of type 'Int' to type '(some P).V'
}
1 Like

Good points above!

Interestingly this also fails:

protocol ValueProtocol {}
extension Int: ValueProtocol {}

protocol P: AnyObject {
    associatedtype Value: ValueProtocol
    var b: Value { get set }
}

class C: P {
    var b: Int = 0
    
    func foo(p: any P) {
        let x = p.b
        p.b = x // ❌ Cannot assign to property: 'p' is a 'let' constant
    }
}

This still make sense to me, however: the inferred type of x is any ValueProtocol, not "the particular Value of this particular type conforming to ValueProtocol behind this particular any P".

It could technically become a compiler/language feature to remember that x came from p.b and preserve its underlying type (similar to what opaque some return types do), but this is pretty niche and stops being useful very quickly. Unless this is already supposed to happen, in which case the error is a compiler bug.

It actually sounds like the approach with a constrained generic function that @jamieQ suggested above is what you actually need for the underlying problem you're modeling here.

4 Likes

That works, but existentials are also fine, as long as they also are constrained—which can only happen via primary associated types.

protocol P<Value> {
  associatedtype Value
  var b: Value { get nonmutating set }
}

extension P {
  func foo(p: any P) {
    p.b = b   // ❌ Cannot assign to property: 'p' is a 'let' constant
    p.b = p.b // ❌ Cannot assign to property: 'p' is a 'let' constant
  }
  
  func foo(p: any P<Value>) {
    p.b = b
    p.b = p.b
  }
}
3 Likes

Regarding the "this error doesn't make sense" problem... what sort of feedback do people think the compiler should offer here?

The first step should probably be to get referring to associated types of opaque types to stop crashing* the compiler:

protocol P { associatedtype T }
let t: (some P).T = () // assigned value doesn't matter

…because these error messages misleadingly suggest that (some P).T is usable in code, and an error message instead of a crash would be more educational:

protocol P {
  associatedtype T
  var t: T { get nonmutating set }
}

extension P {  
  func f(p: some P) {
    p.t = t // Cannot assign value of type 'Self.T' to type '(some P).T'
    t = p.t // Cannot assign value of type '(some P).T' to type 'Self.T'
  }
}

Is there a reason you can't just use the same concept as "(some P).T" for the error? I.e.

Cannot assign value of type 'Self.T' to type '(any P).T'

extension P {
  func f(p: any P) {
    p.t = t // Cannot assign to property: 'p' is a 'let' constant 👎
    t = p.t // Cannot assign value of type 'Any' to type 'Self.T' 👍
  }
}

* Switching to any does not crash.

Cannot access associated type 'T' from 'any P'; use a concrete type or generic parameter base instead

Seems that may have already been fixed on main.

Perhaps, though I'm not sure if that wording would actually clarify the problem/solution to many people.


At any rate, I took the liberty of filing this bug report: Misleading/incorrect diagnostic when assigning to mutable property of an existential · Issue #88837 · swiftlang/swift · GitHub

1 Like

The diagnostic suggested by @Danny is clear to me. If you'd like it to be more informative, how about this?

error: cannot assign to (any P).Value, which could have a type other than Int.

FWIW, the following code used to have the same issue (it's from an old thread):

protocol Named {
    var name: String { get set }
}

struct Person: Named {
    var name: String = "John Doe"
}

struct Cat: Named {
    var name: String = "Missy"
}

func rename(_ named: inout Named) {
    named.name = "Foo"
}

var person = Person()
rename(&person)

It produced an incorrect diagnostic on Swift 4.2 (it's the last version before Swift 5.0 available on Compiler Explorer):

error: cannot pass immutable value as inout argument: implicit conversion from 'Person' to 'Named' requires a temporary

but a nice one since Swift 5.0:

error: inout argument could be set to a value with a type other than 'Person'; use a value declared as type 'any Named' instead

1 Like