Why can actors have non-sendable parameters?

I am trying to understand the interaction between Actors and sendable but it seems I still don't get it. After reading the Sendable evolution proposal, I was under the impression that we cannot call functions on actors with non-sendable parameters, this is the snippet I was reading:

actor SomeActor {
  // async functions are usable *within* the actor, so this
  // is ok to declare.
  func doThing(string: NSMutableString) async {...}
}

// ... but they cannot be called by other code not protected
// by the actor's mailbox:
func f(a: SomeActor, myString: NSMutableString) async {
  // error: 'NSMutableString' may not be passed across actors;
  //        it does not conform to 'Sendable'
  await a.doThing(string: myString)
}

however when I try and implement something similar, I don't get an error when I expect I should get one, here is a very stupid example I conjured quick

class User {
  var name: String
  var password: String

  init(name: String, password: String) {
    self.name = name
    self.password = password
  }
}

actor LastUserLogger {
  var lastUser: User?

  func lastUser(was user: User) {
    self.lastUser = user
  }
}

@MainActor
class SomeViewModel {
  let actor = LastUserLogger()
  var user: User?

  func doStuffWithUser(name: String, password: String) async {
    let user = User(name: name, password: password)
    self.user = user
    await actor.lastUser(was: user)
  }

  func resetPasswordOnLast() async {
    user?.password = "new password"
  }
}

I am for some reason allowed to pass the User between the MainActor and the LastUserLogger actor and keep reference in both places. Should only Sendable types be allowed to cross isolation domains?

I need to put my sanity back in place so any feedback is welcomed :slight_smile:

This looks like a bug because all of the following are diagnosed:

  • Removing @MainActor from the class
  • Changing the class to an actor
  • Changing await actor.lastUser(was: user) to await actor.lastUser(was: self.user!)

If I remove @MainActor I get

await actor.lastUser(was: user) // Sending 'user' risks causing data races

but shouldn't I get this error even if @MainActor is present?

PS: I double checked and my project Swift Version is set to 6

Yeah I think that's the bug. If @MainActor is present, it thinks the store self.user = user is safe, but it isn't, given that the value is passed to the actor method. At least, that's my understanding here.

Changing lastUser() to use sending also results in the correct diagnostic:

func lastUser(was user: sending User)
1 Like

curiously, marking the SomeViewModel type explicitly as final also appears to make the expected errors arise (on a recent nightly snapshot anyway). i encourage you to file this as a bug since it appears to be a data-race safety hole.

3 Likes