Complete checking with an incorrectly-annotated init conformance?

I have discovered a situation that I am unable to find a warning-free solution for. Here's the issue:

// this is not under my control, but I *know* that this init will always be MainActor isolated.
protocol RequiresInit {
	init()
}

class NonSendable {
}

@MainActor
class MyClass: RequiresInit {
	let value: NonSendable

	required nonisolated init() {
		self.value = NonSendable() // how can I initialize this without warnings?

		// this doesn't work...
		MainActor.assumeIsolated {
			self.value = 5 // Cannot assign to property: 'value' is a 'let' constant
		}
	}
}

I'm unable to come up with a combination that works here. Do I have any options?

Edit: I accidentally went too far and over-simplified my problem. The above is a more accurate version of the problem than what I originally posted.

1 Like
required init() {
    MainActor.preconditionIsolated()
    self.value = 5
}

Note that the compiler is still going to warn you that the isolated init cannot satisfy the protocol's requirements (although in Swift 5.9.2, at least, it still does - this will surely break in Swift 6… though oddly, in 5.9.2 with 'complete' concurrency checking it still does not).

1 Like

I had to edit the original post because I over-simplified the problem. Sorry about that!

However, I'm not certain it's even worthwhile checking these kinds of things with anything less than a 5.10 compiler. In fact, I thought had a solution to this problem with 5.9.2, but 5.10 made it harder!

Your revised code doesn't emit any warnings in 5.9.2.

Yes that is correct! I was able to find numerous "solutions" for 5.9, but all exploit holes that have been closed in 5.10.

Oh, good find. I think the only way to suppress this warning in 5.10 is to initialize the isolated property through a Sendable type, e.g.

protocol RequiresInit {
  init()
}

class NonSendable {
}

struct Transferred<T>: @unchecked Sendable {
  let value: T
}

@MainActor
class MyClass: RequiresInit {
  private let _value: Transferred<NonSendable>
  var value: NonSendable {
    _value.value
  }

  required nonisolated init() {
    self._value = Transferred(value: NonSendable())
  }
}

This is sort of emulating what region isolation can do, and there's no reason why region isolation wouldn't be able to determine that your original code is safe; NonSendable() is clearly in a disconnected region, so it's safe to transfer that into the @MainActor region. It still warns on main with -enable-experimental-feature RegionBasedIsolation, but as far as I can tell this is only because the conservative warning coming from the actor isolation checker isn't suppressed when region isolation is enabled.

4 Likes

If you find yourself doing this a lot, you can introduce a little property wrapper (or macro) to help reduce the code churn required, which you can then easily audit later on:

class NonSendable {}

// To be used carefully for transferring non-Sendable
// values to actor-isolated stored properties in nonisolated
// initializers. All uses of this may be removed once
// region isolation in SE-0414 is enabled.
@propertyWrapper
struct InitializerTransferred<T>: @unchecked Sendable {
  let wrappedValue: T
}

@MainActor
class MyClass {
  @InitializerTransferred var value: NonSendable

  required nonisolated init() {
    // Note: 'NonSendable()' is in a disconnected region -- no other value can
    // reference it -- so it's safe to transfer to the main actor.
    self._value = InitializerTransferred(wrappedValue: NonSendable())
  }
}
2 Likes

Thank you so much! In fact I do have to do this in a number of places, and the @propertyWrapper was a great idea.