Swift 6 Key path compiler bug or human misunderstanding?

I'm trying to understand a key path error when porting an app to Swift 6 language mode using Xcode 16 beta 4.

The error from the compiler is:

error: cannot form key path to main actor-isolated property 'rebuildDatabase'
  @AppSetting(\.rebuildDatabase) private var rebuildDatabase
                ^

for the following simplified code:

@MainActor
final class AppSettings {
  var rebuildDatabase: Bool = false

  static let shared = AppSettings()
}

@MainActor
struct MyApp {
  @AppSetting(\.rebuildDatabase) private var rebuildDatabase
}

@MainActor
@propertyWrapper
struct AppSetting<Value> {
  private let keyPath: ReferenceWritableKeyPath<AppSettings, Value>
  private let preferences: AppSettings

  init(_ keyPath: ReferenceWritableKeyPath<AppSettings, Value>) {
    self.keyPath = keyPath
    self.preferences = .shared
  }

  var wrappedValue: Value {
    get { preferences[keyPath: keyPath] }
    nonmutating set { preferences[keyPath: keyPath] = newValue }
  }

  static subscript<T>(
    _enclosingInstance instance: T,
    wrapped wrappedKeyPath: ReferenceWritableKeyPath<T, Value>,
    storage storageKeyPath: ReferenceWritableKeyPath<T, Self>
  ) -> Value {
    get {
      instance[keyPath: storageKeyPath].wrappedValue
    }
    set {
      instance[keyPath: storageKeyPath].wrappedValue = newValue
    }
  }
}

Everything happens on the MainActor, why is this an issue? I would be very grateful for any help in fixing the error or understanding the problem.

My current understanding is that behavior from swift-evolution/proposals/0411-isolated-default-values.md at main · swiftlang/swift-evolution · GitHub

If you move initialization of a wrapper into init, it should be fine.

1 Like

Thank you @vns, that works indeed, but it's pretty unergonomic now.

@MainActor
struct MyApp {
  private var rebuildDatabase: AppSetting<Bool>

  init() {
    rebuildDatabase = AppSetting(\.rebuildDatabase)
  }
}

Is there no other solution than moving everything from the nice property wrapper annotation to init? AppSettings runs on the MainActor to avoid data races when accessing settings, I could make it non-isolated but then other means to prevent data races are required.

After reading the swift-evolution proposal 411 I still do not understand the error. The implicit initializer of MyApp is isolated to the MainActor hence the implicit initialization of rebuildDatabase should also be done on the MainActor, right? All participating code fragments are isolated to MainActor, still there is an error from the compiler.

I cannot elaborate more on the topic, unfortunately. I might as well misunderstand that part from SE-411 and current behaviour is actually should be addressed by the compiler. My understanding initially was the same as yours: everything main actor isolated, that should work – just yesterday it was the time for me to address this issue in my codebase with @dynamicMemberLookup, and moving to init was the only way for me to fix it, and this reasoning from SE-411 came after.

1 Like

I worked around the compiler error by only MainActor-isolating the shared property of AppSettings and passing all accesses to AppSettings through the AppSetting property wrapper, which is isolated to MainActor. This is race-free, but I firmly believe the compiler should accept the initial sources since they are also race-free (until proven otherwise of course).

final class AppSettings {
  var rebuildDatabase: Bool = false

  @MainActor static let shared = AppSettings()
}

@MainActor
struct MyApp {
  @AppSetting(\.rebuildDatabase) private var rebuildDatabase
}

actor DB {
  func f() async {
    let v = await MainActor.run {
      AppSettings.shared.rebuildDatabase
    }
  }
}

@MainActor
@propertyWrapper
struct AppSetting<Value> {
  private let keyPath: ReferenceWritableKeyPath<AppSettings, Value>
  private let preferences: AppSettings

  init(_ keyPath: ReferenceWritableKeyPath<AppSettings, Value>) {
    self.keyPath = keyPath
    self.preferences = .shared
  }

  var wrappedValue: Value {
    get { preferences[keyPath: keyPath] }
    nonmutating set { preferences[keyPath: keyPath] = newValue }
  }

  static subscript<T>(
    _enclosingInstance instance: T,
    wrapped wrappedKeyPath: ReferenceWritableKeyPath<T, Value>,
    storage storageKeyPath: ReferenceWritableKeyPath<T, Self>
  ) -> Value {
    get {
      instance[keyPath: storageKeyPath].wrappedValue
    }
    set {
      instance[keyPath: storageKeyPath].wrappedValue = newValue
    }
  }
}
1 Like

https://github.com/swiftlang/swift/issues/75616

The issue is gone using Xcode 16 beta 6.

2 Likes