How much do you want to dig into the rationale behind Swift's memory model?
I don't think the code in the example is problematic, except that it violates exclusivity rules. It's sort of like how if a train is on a set schedule, but the driver sees someone running to board as its just starting to accelerate, maybe it'll stop and let the person on. Except Swift does the strict thing because it's a compiler and not a person, and doesn't stop accelerating.
Maybe a simple explanation is that Swift needed to come up with some formal rules for how it manages storage/memory access. The potential benefit of coming up with rules is that Swift could gain performance by making fewer copies of values if it knew it could trust that the whole program was obeying the same ruleset, and the other benefit is that you avoid "spooky action at a distance" with accidentally shared mutable state.
One of the rules is that a program can't perform a read on a particular memory location while another part of that program is already performing a write operation on that memory location. And it turns out that write operations can be non-instantaneous, like for the whole duration of a function call (and that function can call other functions, and so on).
In the example you cited; stepSize
is marked as being mutated for the whole duration of increment(_:)
. The code inside increment(_:)
tries to read stepSize
, and that's when the exclusivity violation occurs.
Personally, I find it easier to see the problem when looking at the generated SIL. I inlined and indented the body of increment(_:)
into the main function so it's a bit easier to see the overlap:
// main
sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
alloc_global @$s4main8stepSizeSivp // id: %2
%3 = global_addr @$s4main8stepSizeSivp : $*Int // users: %6, %7
%4 = integer_literal $Builtin.Int64, 1 // user: %5
%5 = struct_ $Int (%4 : $Builtin.Int64) // user: %6
store %5 to %3 : $*Int // id: %6
// âś… Here's where the modify access to `stepSize` starts.
%7 = begin_access [modify] [dynamic] %3 : $*Int // users: %10, %9
// function_ref increment(_:)
%8 = function_ref @$s4main9incrementyySizF : $@convention(thin) (@inout Int) -> () // user: %9
%9 = apply %8(%7) : $@convention(thin) (@inout Int) -> ()
// This part here is where increment(_:) is called.
sil hidden @$s4main9incrementyySizF : $@convention(thin) (@inout Int) -> () {
// %0 // users: %6, %2
bb0(%0 : $*Int):
%1 = global_addr @$s4main8stepSizeSivp : $*Int // user: %3
debug_value_addr %0 : $*Int, var, name "number", argno 1 // id: %2
// ❌ Here's where the conflict happens, we're reading a memory location
// that another part of the code already has exclusive access to.
%3 = begin_access [read] [dynamic] %1 : $*Int // users: %4, %5
%4 = load %3 : $*Int // user: %9
end_access %3 : $*Int // id: %5
%6 = begin_access [modify] [static] %0 : $*Int // users: %16, %7, %18
%7 = struct_element_addr %6 : $*Int, #Int._value // user: %8
%8 = load %7 : $*Builtin.Int64 // user: %11
%9 = struct_extract %4 : $Int, #Int._value // user: %11
%10 = integer_literal $Builtin.Int1, -1 // user: %11
%11 = builtin "sadd_with_overflow_Int64"(%8 : $Builtin.Int64, %9 : $Builtin.Int64, %10 : $Builtin.Int1) : $(Builtin.Int64, Builtin.Int1) // users: %13, %12
%12 = tuple_extract %11 : $(Builtin.Int64, Builtin.Int1), 0 // user: %15
%13 = tuple_extract %11 : $(Builtin.Int64, Builtin.Int1), 1 // user: %14
cond_fail %13 : $Builtin.Int1 // id: %14
%15 = struct_ $Int (%12 : $Builtin.Int64) // user: %16
store %15 to %6 : $*Int // id: %16
%17 = tuple ()
end_access %6 : $*Int // id: %18
%19 = tuple () // user: %20
return %19 : $() // id: %20
} // end sil function '$s4main9incrementyySizF'
// âś… Here's where the modify access ends.
end_access %7 : $*Int // id: %10
// After this it would be okay to access `stepSize` in other code.
%11 = integer_literal $Builtin.Int32, 0 // user: %12
%12 = struct_ $Int32 (%11 : $Builtin.Int32) // user: %13
return %12 : $Int32 // id: %13
}