Currently (in Swift 6.2) the following code does not compile:
actor Foo {
let bar: Bar
init() {
self.bar = .init(constant: 7)
}
nonisolated var constant: Int {
bar.constant // Error: Actor-isolated property 'bar' can not be referenced from a nonisolated context
}
}
class Bar {
let constant: Int
init(constant: Int) { self.constant = constant }
}
First of all, it seems unnecessarily strict to say that I can’t even referencebar from my nonisolated context.
But, of course, simply being able to reference bar wouldn’t help me here. What I would need is for the compiler to see that Bar.constant is an immutable stored property and to allow the access. Can we allow this for non-public types?
I understand that for public types this would be problematic because we want all public promises to be explicit, and this would turn into an implicit promise that the property will always be immutable, rather than just read-only. But for non-public types this seems potentially non-problematic and very convenient in some situations.
actor Foo {
let bar: Bar
let barSendable: BarSendable
init() {
self.bar = .init(constant: 7)
self.barSendable = bar.sendable
}
nonisolated var constant: Int {
barSendable.constant
}
}
final class BarSendable: Sendable {
let constant: Int
init(constant: Int) { self.constant = constant }
}
class Bar {
let sendable: BarSendable
var constant: Int { sendable.constant }
init(constant: Int) { self.sendable = BarSendable(constant: constant) }
}
I think to support this, we need to be able to disambiguate between default-isolated and nonisolated members of non-sendable types:
actor Foo {
let bar: Bar
init() {
self.bar = .init(constant: 7)
}
nonisolated var constant: Int {
bar.constant // ok
bar.getDoubleConstant() // ok
bar.bad() // ok, but error in the implementation
bar.notGood() // error
}
}
class Bar {
nonisolated let constant: Int
var other: Int
init(constant: Int) {
self.constant = constant
self.other = 0
}
nonisolated func getDoubleConstant() -> Int {
return 2 * constant // ok
}
nonisolated func bad() -> Int {
return other // error: cannot access default-isolated member of non-sendable class Bar in isolated context
}
func notGood() -> Int {
return other // ok, but error at the use site
}
}
Yes, I hadn’t considered that the let could be overridden in a subclass. That’s annoying, because it’s very unlikely.
I don’t think we allow this even if the class or the property is final. Without an explicit final, I’m not sure there’s a reasonable approach for addressing this, unfortunately.
Do I understand correctly that my proposed modification to the isolation rules is still doable for final classes/properties, just not for non-final ones? To me it seems worth it to make this tweak even if it only benefits final classes. I personally have only written final classes for many years now and I think that’s probably not uncommon among present-day Swift developers.
I believe the current rule is that we treat actor let properties of sendable type as nonisolated, and therefore allow them to be read from nonisolated contexts. My interpretation of the request here is that we should also allow reads of properties of non-sendable type as long as they are only intermediate steps in an access that ultimately ends with a value of sendable type. For that to work, all of the properties involved must be restricted such that they can be safely read without from a nonisolated context, which mostly means we have to statically know that they’re lets, although being explicitly nonisolated would also be sufficient if the type otherwise has some kind of innate isolation.
I'm trying to think if this can be factored down into simpler composable rules.
First observation is that references to non-sendable classes are not inherently unsafe to pass around between different isolation domains. It is dereferencing in the wrong domain that can cause problems. The reason references are forbidden to be passed around, is that currently when passing such reference to a different domain, compiler forgets which actor/region the object was isolated to and cannot check if dereferencing is safe. If this restriction can be solved/softened, then reference stored in nonisolated let could be read (but not dereferenced) from any isolation domain.
We need something like this (pseudocode):
actor Foo {
nonisolated let bar: @isolated(self) Bar
}
We don't necessarily need new syntax, @isolated(self) can be inferred for non-Sendable types used inside an actor. But under the hood, this requires compiler to distinguish between isolation of the variable (nonisolated) and isolation of the type (isolated to self).
Next, we need to understand which members of the Bar can and cannot be accessed from all isolation domain. I find it helpfull to think about non-Sendable types as region-isolated. Mutable variables inside the object, inherit region of the object. But immutable ones can be nonisolated (= accessible from all isolations regardless of the region). Non-Sendable variable types also inherit region of the object. Currently such distinction exists neither in syntax, nor as a concept in compiler's algorithms.
In terms of syntax, I think it makes sense to use explicit nonisolated keyword to mean "accessible from all isolations regardless of the region", and no isolation attributes to mean "region-isolation". nonisolated could be inferred for let properties, but needs to be explicit for methods that access them.
Something along these lines I've been looking for is some kind of strategy to enforce "dimensions" of sendability… where I can specify that a variable is "unsafe" WRT the access of that variable but also "safe" WRT the value itself.
Yes, the general property here is that the computation needs to not rely on any isolation not statically provided by the current context. A nonisolated method on a Sendable type probably does have to have that property, although I'd need to think about it. A nonisolated method on a non-Sendable type definitely does not have that property; that code is valid today, and it does not have the restrictions it would need to make that correct, and imposing them would be a significant strengthening of the current rule. It is a logical interpretation, though.
Note that it's fine to "re-enter" the current isolation, which can happen with global actors. For example, if both z and the current context are @MainActor, x.y.z is also valid as long as x and y are nonisolated (by your definition).