"Conflicting Access to In-Out Parameters" some case seem to have undefined behavior

I'm using Xcode 10 and Playground for test, code is work without error?

Below is the "The Swift Programming Language (Swift 4.2)"

A function has long-term write access to all of its in-out parameters. The write access for an in-out parameter starts after all of the non-in-out parameters have been evaluated and lasts for the entire duration of that function call. If there are multiple in-out parameters, the write accesses start in the same order as the parameters appear.

One consequence of this long-term write access is that you can’t access the original variable that was passed as in-out, even if scoping rules and access control would otherwise permit it—any access to the original creates a conflict. For example:

var stepSize = 1

func increment(_ number: inout Int) {
    number += stepSize
}

increment(&stepSize)
// Error: conflicting accesses to stepSize

That is a consequence of SE-0176 Enforce Exclusive Access to Memory, which was implemented in Swift 4. You'll find more information and examples in the proposal.

In your case – if I understand it correctly – it is unsafe to access stepSize inside func increment(), because the same variable is passed as in-out parameter to the function.

2 Likes

I wouldn't call particularly that example unsafe, but yes, your case falls under the first part of the motivation section of SE-0176. There shouldn't be an error in a playground though.

The error is correct. You could say "oh, in this case it's safe because we only read from stepSize before doing any modification of the inout arguments", but that makes an already subtle language rule even more complicated. Instead, we went with a simpler rule: "when a variable is passed inout, it cannot be read from or written to until the function call is over".

3 Likes

Was an exception made for playground top-level stuff, for which the error doesn't appear at runtime in Xcode 10? I don't see how the specific rule you stated could differ for a playground.

It's easy to see how quickly code like this becomes unsafe:

func increment(_ number: inout Int) {
    number += stepSize
    //...
    number += stepSize
}

It's better to give developers a simple understandable rule to follow.

I don't see how that is unsafe either. Regardless, I agree with Jordan as long as narrowing the rule involves significant unwanted complications to the compiler.

This rule was not narrowed, it's always been there. Previously, my example above with increment(stepSize) was unspecified behavior. The code is also ambiguous. Anyone who mentally maps inout to pass-by-reference will misunderstand it.

I didn’t say it was narrowed, I meant if it were to be narrowed. Could you explain why this is (or was) unspecified behavior?

What do you think this should print and does the swift compiler agree with you?

var stepSize = 1
func increment(_ x: inout Int) {
  x += stepSize
  x += stepSize
}
increment(&stepSize)
print(stepSize)

Here's a fun example of code that behaves completely wrong without inout being considered a write for the entire duration of the function:

func increment(_ i: inout Int) {
  var totallyACopyNothingToSeeHere = array
  totallyACopyNothingToSeeHere[0] += 1
  i += 1
}

var array = [1]
increment(&array[0])
print(array) // [3]

Array's subscript pins its buffer for the mutation of &array[0], so when attempting to mutate it again within that window, it modifies in-place rather than re-allocating.

If this were allowed I would guess it should print 3 because of the write-back-on-return model, but I am happy that it is not allowed.

Considering how inout works, that should make stepSize equal to 3, since the value the in-out parameter is assigned isn't passed out of the function until it returns. If it were simply a reference, stepSize would end up being 4. Currently, in an Xcode 10 playground, the result is 4, which brings back the question I addressed to @jrose

If you don't have an older Xcode installed, you can try it yourself like this:
swiftc ./t.swift -enforce-exclusivity=unchecked

Thanks for the example @hamishknight.

I don't know what you're talking about with playgrounds. It's supposed to be an error everywhere. If it's not that's a bug.

@anandabits just to prove that you're not wrong, you can also try this one:

struct S<T: SignedInteger> {
  var stepSize: T
  func increment(_ x: inout T) {
    x += stepSize
    x += stepSize
  }
}

var s = S(stepSize: 1)
s.increment(&s.stepSize)
print(s.stepSize)

swiftc ./t.swift -enforce-exclusivity=unchecked
(hint: you'll get a different answer, which is what we mean by unspecified behavior).

1 Like

SR-8126 Well, bug means bug.

@Andrew_Trick I was confusing unspecified behavior with UB. What is the main reason behind enforcing such a broad rule instead of fixing the behavior to be specified and consistent with the current language rules where it can be? Note that I agree sometimes access exclusivity is beneficial. For instance, when a value might be or is being simultaneously accessed in a completely different place or thread.

Right, I was specifying what I would expect if the behavior of this code was specified, but as I mentioned above I am glad that it isn't supported.

Doesn't the exclusive access rule partially exist to allow the compiler to optimize inout into pass-by-reference in more cases?

A rule that simply prohibits simultaneous access to the same variable or property cannot be misinterpreted. Once it's fully enforced, it can't be ignored either.

We could make a rule specifying a particular behavior instead, but that would always be counterintuitive to some set of developers and some code patterns. I don't know where you would draw the line with "obvious" behavior.

We do loosen enforcement for struct properties, when two accesses are to obviously different memory locations within the same struct, since there can be no debate about the proper semantics in that case.

@hamishknight posted an example above illustrating why we don't allow this. I also think the trivial example that I posted would be misleading for anyone who interprets Swift's '&' parameter as pass-by-reference.

Exclusivity does not protect threads from accessing the same shared property or global.