Overlapping accesses to 'self', but modification requires exclusive access

how come we are not allowed to pass the fields of a struct inout in a consuming method?

struct S {
    var x: Int
    var y: Int
}
extension S {
    consuming func f() {
        { $0 += 1 ; $1 += 1 } (&self.x, &self.y)
//                              ~~~~
// Overlapping accesses to 'self', but modification requires exclusive access
    }
}

weirdly enough, the problem goes away if i make S noncopyable?

- struct S {
+ struct S: ~Copyable {

here’s what an LLM told me, which i felt was ill-informed and not correct even for mutating methods

LLM transcript

This error occurs because you are violating Swift’s Law of Exclusivity (specifically, SE-0176).

Even though x and y are distinct fields, passing them both as inout parameters in the same function call requires simultaneous write access to the root self.

Here is the breakdown of why this happens and how to fix it.

The Core Conflict: Simultaneous Write Access

When you pass a property as inout (e.g., &self.x), Swift requires exclusive write access to the entire aggregate value (the struct instance) to ensure memory safety.

The compiler sees the code roughly like this:

  1. Argument 1 (&self.x): "I need to lock self for writing so I can modify x."
  2. Argument 2 (&self.y): "I also need to lock self for writing so I can modify y."
  3. The Call: Both arguments are evaluated and passed to the closure simultaneously.

Because both arguments try to "lock" self for writing at the exact same time, the compiler flags an overlap. It cannot guarantee that modifying x won't interfere with y (or vice versa) in a way that corrupts the memory of self.

Why does this work for local variables?

You might have noticed that this pattern works perfectly fine if you define a local variable:

Swift

func test() {
    var s = S(x: 1, y: 2)
    // This WORKS:
    { $0 += 1; $1 += 1 }(&s.x, &s.y) 
}

Swift includes a specific exception for local stored properties. If the compiler can see the full definition of a local struct variable, it can mathematically prove that x and y occupy different memory addresses and do not overlap.

However, self is special.

Even in a consuming method (where self is owned by the function), self is treated as a parameter, not a standard local variable. The Swift compiler is much stricter with parameters; it does not apply the "disjoint property access" exception to self. It conservatively assumes that writing to any part of self requires locking the whole of self.

The Solution: Use a Local Copy

Since consuming gives you ownership of self, you can move it into a purely local variable. The compiler will then treat it with the relaxed rules allowing disjoint access.

Swift

struct S {
    var x: Int
    var y: Int
}

extension S {
    consuming func f() {
        // 1. Move self into a local variable
        var copy = self 
        
        // 2. Perform the operation (now allowed on 'copy')
        { $0 += 1 ; $1 += 1 } (&copy.x, &copy.y)
        
        // 3. (Optional) If you need to reassign self or return it, do so here.
        // Since this is consuming, 'self' is effectively gone anyway.
        print(copy)
    }
}

Summary

  • The Rule: You cannot have two active inout accesses to the same variable (or its parts) at the same time.
  • The Exception: Swift allows this for different properties of local variables only.
  • The Gotcha: self is a parameter, not a local variable, so the exception does not apply. Move it to a var to fix it.
2 Likes

This does seem like a bug. Swift is generally supposed to permit simultaneous mutation of different stored properties of a struct, and while there are some reasonable limits to the analysis, this should be well within them. I would guess that the annotations we're adding to consuming methods are interfering with the diagnostic pass.

2 Likes

Struct fields are not currently considered independent for the purposes of exclusive access, since historically, we wanted to fully abstract away the layout of types as part of their API, allowing for their replacement by computed properties and so on. We've broken that barrier for other noncopyable analyses, but not yet for exclusivity checking. There's no fundamental reason we couldn't, but in the meantime, you might be able to take advantage of the fact that we do support partial consumption, and do something like let x = self.x; let y = self.y; update(&x, &y); self = S(x: x, y: y).

2 Likes
1 Like

are there any further qualifications to this? i was under the impression that they were considered independent, at least in some cases (like pointed out here).

Noncopyable-type-specific analyses should be field aware (so long as the type in question is actually locally-defined and/or @frozen). Exclusivity checking is not noncopyable-specific and as yet has not been updated to be field aware (at least, not intentionally, as far as I know). Definite initialization in init definitions is field-aware for self by necessity, but that's the only possible exception I can think of.

2 Likes

I don't believe this is generally true:

struct Pair {
  var x: Int
  var y: Int
}

func test1() {
  var pair = Pair(x: 0, y: 1)
  swap(&pair.x, &pair.y) // no diagnostic
}

func weirdSwap(_ x: inout Int, _ pair: inout Pair) {}

func test2() {
  var pair = Pair(x: 0, y: 1)
  weirdSwap(&pair.x, &pair) // error: overlapping accesses to 'pair.x'
}

Yes, it needs to be limited to cases where you know that these are stored properties. Swift appears to currently make use of all information known to the compiler, i.e. we do assume that we can independently access stored properties of non-frozen structs in modules that aren't compiled with library evolution. That is arguably bad for source evolution in the same way that exhaustive switches on non-frozen enums are.

8 Likes

It seems like the analysis that determines when overlapping field accesses are known to be disjoint is quite limited. Specifically, modifying this example slightly gives rise the following two failure cases:

// making the struct non-copyable leads to an error
struct Pair: ~Copyable {
  var x: Int
  var y: Int
}

func test1() {
  var pair = Pair(x: 0, y: 1)
  swap(&pair.x, &pair.y) // error: overlapping accesses to `pair`
}
struct Pair {
  var x: Int
  var y: Int
}

// factoring out the swap call into a helper leads to an error
func test1() {
  var pair = Pair(x: 0, y: 1)
  helper(consume pair);
}

// if `consuming` is replaced by `inout`, this works perfectly fine.
func helper(_ pair: consuming Pair) {
  swap(&pair.x, &pair.y)  // error: overlapping accesses to `pair`
}

Yeah, that's very likely the same basic problem as with consuming parameters. It's a given that the analysis is checking for a specific code pattern and is therefore "brittle" in a sense. These code patterns just need to be brought within that brittleness. The analysis was written prior to the addition of consuming and ~Copyable types and therefore needs to be extended to check for the slightly different SIL patterns generated in these cases.

2 Likes