I was reminded of a problem I ran into recently, and thought I’d check in to see what others think. Today, there’s really no way do something like this:
class NonSendableInput {
let value: Int = 0
}
class NonSendableOutput {
let value: Int
init(_ d: NonSendableInput) {
// this is safe to do, but not visible to callsites
self.value = d.value
}
}
func returnsSending(_ d: NonSendableInput) -> sending NonSendableOutput {
// error: Task or actor isolated value cannot be sent
return NonSendableOutput(d)
}
The problem is, I think, that the initializer causes the result to be merged into the d region, which prevents it from being sent. This makes sense, but is a false-positive. However, I know of no way to work around this without introducing some kind of static construction method.
Could something like this make sense?
class NonSendableOutput {
sending init(_ d: NonSendableInput) {
}
}
I've been having a slightly related issue with inits and Swift Concurrency: actors sometimes can't use property wrapper inits, and can never use property initialization with @storageRestrictions. I think being able to author sending inits for propertyWrappers could potentially help with the former issue.
And now that I’m thinking about it, here’s another one. I cannot recall encountering this particular problem, but it does seem like a very related limitation.
class NonSendable {
var sendingGet: NonSendable {
get {
NonSendable()
}
}
}
func returnsSending(_ ns: NonSendable) -> sending NonSendable {
// Sending 'ns.sendingGet' risks causing data races
return ns.sendingGet
}
actor A {
init(a: Int) {
self.a = a // Actor-isolated property 'a' can not be mutated from a nonisolated context
}
var a: Int {
@storageRestrictions(initializes: _a)
init {
_a = newValue
}
get {
_a
}
set {
_a = newValue
}
}
var _a: Int
}
actor A2 {
init(a: Int) {
self.a = a // Actor-isolated property 'a' can not be mutated from a nonisolated context
}
@P
var a: Int
}
@propertyWrapper
struct P {
init(wrappedValue: Int) {
self.wrappedValue = wrappedValue
}
var wrappedValue: Int
}
I think the fix for the first is to add a language feature that allows property inits to have the same psuedo-nonisolated behavior that a synchronous actor init has. Unfortunately I think that just adding @StorageRestrictions(initializes: ) nonisolated init as valid Swift could create ambiguities or complications parsing computed vars with an implicit getter, but not sure about that.
An alternative design to the above would be to infer that behavior, but that would limit asynchronous actor inits. There's a comment in the Swift compiler source that explicitly rules this alternative out (I think @hborla is the author but I don't have it handy to check).
Pretty sure adding your sending init should fix the second by banning capturing self in a Task or something else that will allow it to escape the current isolation domain.
Thanks for sharing these! Please do correct me if I’m wrong here, because I want to make sure I understand. However, I think these are both actually manifestations of the same underlying issue, and one that a sending init construct could not help with.
A synchronous init for an actor has a nonisolated self reference. So, even if you could return a sending value from one of these properties, you still could not actually call them synchronously unless they were also nonisolated. Their internals might depend on actor-isolated stuff.
And on top of that, right now nonisolated property wrappers are unsupported. I’ve hit this limitation myself on a few occasions, got fired up, did some research on what might be involved to fix it, and chickened out.
Ah, yeah. that's true. I was thinking that the compiler was confused about the property wrapper instance and thought it was isolated to self, but yeah you're right that's not the actual error message, and that it is the same as the storageRestrictions one. So yeah probably the same issue.