I don't know if this has been reported before, but I seem to have found a bug in the compiler that causes a runtime crash.
Can anyone weigh in as to what may be happening? It's entirely possible that I'm doing something wrong, of course.
Here's the example code snippet that can reproduce the crash:
func example() {
var executionPath = ExecutionPath()
print(executionPath.traceMessageList)
// []
executionPath.traceMessageList.append("outer")
// will change traceMessageList from [] to ["outer"]
// did change traceMessageList from [] to ["outer"]
print(executionPath.traceMessageList)
//-------------------------
// Thread 1: EXC_BAD_ACCESS
//-------------------------
executionPath.traceMessageList.append("inner")
print(executionPath.traceMessageList)
}
// An example type simplifying a more elaborate solution for the purpose of
// this discussion.
struct ExecutionPath {
init() { _traceMessageList = .init() }
private var _traceMessageList: [String]
// This is a representation of a property after being modified by an
// accessor macro that transforms the original property with `willSet`
// and/or `didSet` accessors into an observing proxy for an existing
// property.
var traceMessageList: [String] {
get { _traceMessageList }
_modify {
// The following two variables have unique names generated by the
// accessor macro to prevent them from interfering with the user
// code that is copied into `do` statements below.
let _priorTraceMessageList = _traceMessageList
var _nextTraceMessageList = _priorTraceMessageList
defer {
do {
// The following variable has a name that is taken from the
// parameter of the `willSet` accessor if one was present,
// or "newValue" if not.
// The existing "newValue" is consumed into this variable
// in order to avoid the unnecessary reference counting
// overhead while renaming the variable.
let newValue = consume _nextTraceMessageList
// The body of the the `willSet` accessor is copied into the
// following `do` statement verbatim.
do {
// The copied code is wrapped into a `do` statement in
// order to avoid possible "invalid redeclaration"
// errors for "newValue".
print("will change traceMessageList from \(traceMessageList) to \(newValue)")
}
// The renamed "newValue" is put back into the original
// variable so that it can be committed into storage later.
_nextTraceMessageList = consume newValue
}
// The "newValue" is consumed when committing it into storage.
//----------------------------------------------------------
// NOTE: Removing this `consume` keyword resolves the crash.
//----------------------------------------------------------
_traceMessageList = consume _nextTraceMessageList
do {
// The following variable has a name that is taken from the
// parameter of the `didSet` accessor if one was present,
// or "oldValue" if not.
let oldValue = consume _priorTraceMessageList
// The body of the the `didSet` accessor is copied into the
// following `do` statement verbatim.
do {
// The copied code is wrapped into a `do` statement in
// order to avoid possible "invalid redeclaration"
// errors for "oldValue".
print("did change traceMessageList from \(oldValue) to \(traceMessageList)")
}
// The "oldValue" is consumed into oblivion explicitly in
// order to avoid possible "unused variable" warnings.
_ = consume oldValue
}
}
yield &_nextTraceMessageList
}
}
}
Here's the development environment used to reproduce the crash:
$ xcodebuild -version
Xcode 16.2
Build version 16C5032a
$ swift --version
swift-driver version: 1.115.1 Apple Swift version 6.0.3 (swiftlang-6.0.3.1.10 clang-1600.0.30.1)
Target: arm64-apple-macosx15.0
EDIT:
My guess is that the code generator emits an unconditional cleanup for _nextTraceMessageList
even when it has been explicitly consumed during assignment to _traceMessageList
, causing the cleanup code to run on the same memory twice.