Exclusive access on members of self

struct S 
{
    var a:Int, 
        b:Int 
    
    mutating 
    func withASetToZero(_ body:() -> ())
    {
        self.a = 0 
        body()
    }
    
    mutating 
    func foo()
    {
        self.withASetToZero 
        {
            // error at compile time
            self.b = 1
        }
    }
}

var s:S = .init(a: 1, b: 2)

s.withASetToZero 
{
    // error at run time
    s.b = 1
}

Why does the foo() method cause a compile error, while the top level code compiles and crashes at run time? why is this not allowed in the first place, the members are being accessed separately?

I think you have conflicting access to self.

The section named "Conflicting Access to self in Methods" in https://docs.swift.org/swift-book/LanguageGuide/MemorySafety.html explains this better than I could.

yes, i know that, but i’m not actually accessing the same data at the same time, they both just happen to be members of the same aggregate. if this is to protect the state of an aggregate during a mutation, there should at least be an exception (maybe in the form of an annotation) for its own member functions.

And i still don’t know why one of them is a runtime error and the other is a compile time error

mutating functions or operations on value types (including inout parameters) semantically replace the entire value when the scope of the access ends, but do not modify it until that point. When you set s.b, s does not yet have the new value from withASetToZero since that access hasn’t ended - hence the exclusivity warning.

Note that on value types, access to any member is an access to self, while on reference types multiple members can be modified simultaneously.

All of this is described in the Law of Exclusivity section of the Ownership Manifesto.

The compile-time vs. runtime error is I think just a limitation of the compiler - I don’t see why both of those shouldn’t be detectable at compile time.

1 Like

Right. Exclusivity is enforced on an entire aggregate value instead of its individual properties. If it were enforced on individual properties, we generally wouldn't be able to do any enforcement at compile time, and the enforcement we'd have to do at execution time would be much more expensive. Those considerations would make enforcing exclusivity impractical in normal builds: the overhead would simply be too high to enable unconditionally. I don't think we'd be comfortable with making it a language rule without any default enforcement at all — that is, making it a C-style undefined-behavior rule and telling users to run a sanitizer to catch violations — so in effect that would kill the rule completely.

There are other arguments against doing property-specific enforcement — it would have problems with copy-on-write types, and it would weaken protections for types that try to preserve invariants across multiple properties — but I think the performance argument is strong enough on its own.

4 Likes

foo function mutates S type and by also calling self.withASetToZero and modifying self from it violates the exclusivity law because of double writes to self

In main:

s.withASetToZero 
{
    // error at run time
    s.b = 1
}

This closure accesses a global variable s. It does not capture anything, so there's no straightforward way for the compiler to see the conflict. i.e. it doesn't know that the global s is the same object as self.

To add to the practical points that @John_McCall made, structs are different from classes not just because we want fast access, but also that we want the compiler and developers to be able to reason about struct mutation as a whole.

In this code, we want the compiler and user to be able to assume that body does not mutate self:

mutating func withASetToZero(_ body:() -> ())
    {
        self.a = 0 
        body()
    }

As a contrived example, some other developer should be able to come along and do this without knowing what body does:

mutating func withASetToZero(_ body:() -> ())
    {
        if (self.b = 0)
          self.a = 0 
        body()
        assert(self.b || !self.a)
    }
2 Likes