The compiler (Swift 6.4/macOS 27.0.1) rejects the following code with 'previousSnapshot' cannot be captured by an escaping closure since it is a borrowed parameter and two locations are indicated (see // rejected! below)
Is this a bug (or bugs) in the compiler? I can maybe be convinced about the precondition although the autoclosure is not declared escaping so I don't know. But the boolean on the second error location is just puzzling?
Is it because of the forced unwrapping? Which is ok (as the precondition verifies). But maybe its implementation is as a closure and a similar error to the precondition line triggers?
I can get away with copying the parameter [or, in some cases, computing the booleans out of the consuming expression although this defeats the shortcutting] but just feels silly and unergonomic.
Am I missing something?
struct ... {
func resources(
previousSnapshot: borrowing Self?,
previousResources: consuming Resources?
) -> sending Resources? {
// Existing resource implies existing snapshot.
precondition(previousResources == nil || previousSnapshot != nil) // rejected!
// Reuse if nothing changed.
if self == previousSnapshot && previousResources != nil {
return previousResources
}
// No previous buffer to reuse or too small to fit.
let needBuffer =
previousResources == nil || previousSnapshot!.size < self.size. // rejected!
...
}
}
Idk about the auto-closure part, but yeah the force-unwrapping one I believe is a compiler bug, or depending on how you view it 'bug' may be too strong, but the compiler is definitely being a bit obtuse.
struct Thing {
var size = 0
}
func borrow(_ thing: borrowing Thing?) { // 'thing' is borrowed and cannot be consumed
var x = thing!.size // Consumed here
var y = thing!.size // Consumed here
}
The clear intention of thing!.size is "borrow thing and copy the field size into x". But the compiler is interpreting the expression as "consume thing and move the field size into x".
It's definitely possible if you coerce it to understand that it's a borrowing with a switch which defaults to borrow:
func borrow(_ thing: borrowing Thing?) {
let x: Int, y: Int
switch thing {
case .some(let thing): x = thing.size; y = thing.size
case .none: fatalError()
}
print("\(x), \(y)")
}
But to be fair consider if size were a ~Copyable type... then thing!.size does actually need to be interpreted as a consume in order to move the noncopyable member out... so presumably somewhere the compiler is missing some smarter "ownership inference?" (idk if that's a term), to prefer borrowing over consuming when both are possible for an expression, at least in this scenario.
Also here's a slightly less verbose way around it that seems to work with the new Ref type:
func borrow(_ thing: borrowing Thing?) {
var ref = Ref(thing!)
var x = ref.value.size
var y = ref.value.size
}
You misunderstood it. Unwrapping borrows the non-copyable value in above code. This was explained in SE-0432:
If the expression refers to a variable or stored property, and is not explicitly consumed using the consume operator, then the baseline mode is borrowing.
The above code consumes the value because of the assignment rather than forced unwrapping.
The || operator also takes an autoclosure on the right-hand side, so both diagnostics have the same root cause. I agree that it's a misleading diagnostic because the closure in question does not actually escape, but I don't know if this is expected to work or if it just needs a better error message.
That is a lot of hidden detail and subtle behavior to understand, and the list is still growing. It will be good if the behavior can be clearly documented (not sure where, the Swift book?) and the diagnostics are improved.