Am I Wrong That Count Can't Change Here?

I was checking to see how well an implementation of String.startPad optimizes when writing it the long way vs the one liner mentioned in Padding Arrays when I noticed something odd.

extension String {
    mutating func startPad(to size: Int, with pad: Character = " ") {
        guard size > count else { return }

        let padding = String(repeating: pad, count: size &- count)
        self = padding + self
    }
    func startPadded(to size: Int, with pad: Character = " ") -> Self {
        with(self) { $0.startPad(to: size, with: pad) }
    }
}

The optimizer doesn't seem to realize that count doesn't change until we reassign self at the end, and calls count's getter twice. This produced surprisingly larger code than I expected. I only got the expected code when I assigned count to a local let binding before I used it, as can be seen here.

Is this a bug, or can count actually change here even though I should have exclusive access to self in a mutating method?

PS. The one liner optimizes just as well as the version with the let binding, but can crash if size is small enough. This may be a feature or a bug, depending on how you look at it, but I'm leaning towards the one that can't crash being a better implementation.

Afaict, this depends on what you mean by "can" and "actually." Even where you have exclusive access to self, you don't have exclusive ownership of any mutable state in ICU, which is the library that does the counting.

What I mean is, is the version where I manually cache count safe, or will it give incorrect results under some circumstances? If it is safe, why doesn't the compiler do this for me? If it's not safe, then how do I actually write this code so that it always produces the correctly padded string?

What you're asking, basically, is whether count is pure. Semantically, I think so? The compiler can't make that assumption because in the general case the result is determined at runtime by a call to a third-party library.

You could file an optimisation bug, anyway. I think this is the kind of thing that the @_semantics attribute is used for, but it seems like Array has received more attention than String.