Making the compiler "smart" about property wrapper access during initialization

Several times I've run into a situation where I want to do something like the following:

@propertyWrapper
struct Wrapper {
  var wrappedValue: Int

  init() {
    self.wrappedValue = 0
  }
}

struct SomeType {
  @Wrapper
  var prop1: Int

  var prop2: Int

  init() {
    self.prop2 = self.prop1
  }
}

This, of course, fails on the self.prop2 = self.prop1 line with the following message:

'self' used before all stored properties are initialized

Because @Wrapper makes prop1 a computed property, accessing it during init constitutes an access to the entire self value, which isn't permitted before all properties have been initialized.

However, the following (somewhat) analogous code compiles successfully:

struct SomeOtherType {
  var prop1: Int = 0

  var prop2: Int

  init() {
    self.prop2 = self.prop1
  }
}

The compiler is smart enough to know that, while all of self may not be initialized, at the very least self.prop1 is initialized and can be freely accessed.

The workaround for the wrapper case is to access wrappedValue through the private storage:

self.prop2 = self._prop1.wrappedValue

Of course, this is by definition what the compiler has already synthesized for us in the computed prop1 getter. Forcing the user to write this breaks the property wrapper metaphor and leaks the underlying abstraction of synthesized getter/setter.

What do people think about allowing the line above (self.prop2 = self.prop1) to compile, since the compiler knows that it is "just" a simple access to prop1. Are there soundness holes that would actually in practice expose the uninitialized self value (perhaps with property wrappers that use the "enclosing self" feature)?

One potential downside here is that using a property wrapper would cease to be explainable in terms of simple sugar—the user would be able to write code with property wrappers that they couldn't also write "by hand" with a straightforward transformation. However, this is already the case via features like projectedValue since the user is unable to define their own $-identifier properties.

cc @hborla who I'm sure has strong opinions here :slight_smile:

4 Likes

I think this is the main problem here, self gets passed to the subscript that implements this functionality. I don't believe this feature has passed evolution, but my guess is that it is something we may want longer term and it is core enough to SwiftUI (and myriad other frameworks that jump on the internal stuff Apple uses :-) to make removing it a non-starter.

UPDATE: Though, on second thought... which function (static subscript or wrappedValue) to call should be able to be detected statically so it may be possible to provide this behavior only for "standard" property wrappers.

1 Like

Yes, the one soundness hole that I can think of is with enclosing-self property wrappers. One option is to simply not allow accessing enclosing-self property wrappers before all of self is initialized, as @George suggested.

I'm not sure how I feel about allowing access to a property wrapper getter before all of self is initialized. If we do that, we'd need to prevent property observers from being run (just like stored properties). That's fine, but it means there would be three different cases to consider when accessing property wrappers in an initializer: 1) accessing the wrapped property before the backing storage is initialized (invalid), 2) accessing the wrapped property after the backing storage is initialized but before all of self is initialized (valid, but does not run property observers), and 3) accessing the wrapped property after all of self is initialized (valid, and property observers are run). This is not consistent with how assignment to a wrapped property works in an initializer, which is re-written to initialization before all of self is initialized, and re-written to a setter call after all of self is initialized.

3 Likes