Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SR-10950] Property wrappers: @autoclosure not working? #53341

Closed
ole opened this issue Jun 17, 2019 · 13 comments
Closed

[SR-10950] Property wrappers: @autoclosure not working? #53341

ole opened this issue Jun 17, 2019 · 13 comments
Assignees
Labels
bug A deviation from expected or documented behavior. Also: expected but undesirable behavior. compiler The Swift compiler in itself property wrappers Feature: property wrappers

Comments

@ole
Copy link
Contributor

ole commented Jun 17, 2019

Previous ID SR-10950
Radar rdar://problem/53414496
Original Reporter @ole
Type Bug
Status Resolved
Resolution Done
Environment

macOS 10.14.5
Swift snapshot 5.1-DEVELOPMENT-SNAPSHOT-2019-06-16-a

$ swift --version
Apple Swift version 5.1-dev (LLVM 42c6b79618, Swift cbfbc8e850)
Target: x86_64-apple-darwin18.6.0
Additional Detail from JIRA
Votes 0
Component/s Compiler
Labels Bug, PropertyWrappers
Assignee @roop
Priority Medium

md5: 504a059be79e3a05b7670b7c6174d327

Issue Description:

Maybe I'm misunderstanding something, but does adding @autoclosure to the init(wrappedValue: ) initializer of a property wrapper actually have any effect?

I copied the sample code for Lazy from the SE-0258 proppoal, and then I initialize the lazy value with a closure that prints when it's getting called.

Paste this code into a file "propertywrapper.swift":

// ===================================
// Lazy type from SE-0258
// ===================================
@propertyWrapper
public enum Lazy<Value> {
  case uninitialized(() -> Value)
  case initialized(Value)
  
  public init(wrappedValue: @autoclosure @escaping () -> Value) {
    self = .uninitialized(wrappedValue)
  }
  
  public var wrappedValue: Value {
    mutating get {
      switch self {
      case .uninitialized(let initializer):
        let value = initializer()
        self = .initialized(value)
        return value
      case .initialized(let value):
        return value
      }
    }
    set {
      self = .initialized(newValue)
    }
  }
}


// ===================================
// Usage
// ===================================

struct N {
    @Lazy var number: Int
}

print("++ Initializing N")
var n = N(number: { () -> Int in
    print("-- Initializing Lazy storage")
    return 23
}())
print("++ Printing number")
print(n.number)

Actual output (Swift 5.1 dev snapshot 2019-06-16a):

++ Initializing N
-- Initializing Lazy storage
++ Printing number
23

Note that "– Initializing Lazy storage" appears before "++ Printing number". I would have expected these lines to be reversed because of the @autoclosure attribute of the init(wrappedValue: ) initializer.

Expected output:

++ Initializing N
++ Printing number
-- Initializing Lazy storage
23
@belkadan
Copy link
Contributor

The autoclosure describes the behavior of how that one stored property is initialized, but it doesn't describe the behavior of the memberwise initializer synthesized for the entire struct. @DougGregor, should it?

@ole
Copy link
Contributor Author

ole commented Jun 17, 2019

For what it's worth, the output is the same if I move the initialization of the property into the struct, like this:

struct N {
    @Lazy var number: Int = { () -> Int in
      print("-- Initializing Lazy storage")
      return 23
    }()
}

print("++ Initializing N")
var n = N()
print("++ Printing number")
print(n.number)

/* prints:
++ Initializing N
-- Initializing Lazy storage
++ Printing number
23
*/

@belkadan This suggest to me that it's irrelevant whether I use the struct's initializer or the property's initializer. Or I'm still misunderstanding what's supposed to happen.

@roop
Copy link
Mannequin

roop mannequin commented Jul 22, 2019

I do think there's a problem here because:

1. The behaviour of the property-wrapped @Lazy doesn't match the built-in lazy:

struct N {
    lazy var number: Int = { () -> Int in
      print("-- Initializing Lazy storage")
      return 23
    }()
}
// '++ Printing number' comes before '-- Initializing Lazy storage'

2. The behaviour of the property-wrapped @Lazy doesn't match what happens when we explicitly do what the property wrapper ought to have implicitly done, like:

struct N { 
    var _number: Lazy<Int> = Lazy<Int>(wrappedValue: { () -> Int in
      print("-- Initializing Lazy storage")
      return 23
      }())
    var number: Int {
      mutating get { return _number.wrappedValue }
      set(v) { _number.wrappedValue = v } 
    }   
}
// '++ Printing number' comes before '-- Initializing Lazy storage'

@belkadan
Copy link
Contributor

Again, it's specifically a problem with the synthesized memberwise initializer, not with the property itself.

@swift-ci create

@roop
Copy link
Mannequin

roop mannequin commented Jul 23, 2019

@belkadan, I'm talking about the case where N is created as just var n = N() (like in @ole's comment), so the synthesized memberwise initializer is not involved. Sorry I didn't make that clear before.

@belkadan
Copy link
Contributor

Ah, you're right, I wasn't reading it correctly at all. Sorry!

@roop
Copy link
Mannequin

roop mannequin commented Jul 25, 2019

@belkadan, @DougGregor:

As it turns out, the memberwise initializer is indeed involved.

Under certain conditions, the implicitly generated memberwise initializer arg for a property-wrapped struct member would be of the the wrapped type rather than the wrapper type.

For example:

@propertyWrapper
struct Wrap {
  var wrappedValue: Int
  init(wrappedValue: Int) { self.wrappedValue = wrappedValue }
}

struct N {
  @Wrap var number = computeThing()
}

func computeThing() -> Int { return 0 }

// Implicit memberwise initializer for N in swiftc -emit-silgen is:
// N.init(number: Int = computeThing())
// (Here, the number arg is Int, not Wrap)

This is currently true even if the init(wrappedValue: ) takes an autoclosure of the wrapped type.

@propertyWrapper
struct Wrap {
  var wrappedValue: Int
  init(wrappedValue: @autoclosure () -> Int) { self.wrappedValue = wrappedValue() }
}

struct N {
  @Wrap var number = computeThing()
}

func computeThing() -> Int { return 0 }

// Implicit memberwise initializer for N in swiftc -emit-silgen is:
// N.init(number: Int = computeThing())
// (Here too, the number arg is Int, not Wrap)

So, the autoclosed expression computeThing() is always evaluated during the initialization of N, whether we use the default initializer (var n = N()), or the memberwise initializer (var n = N(number: computeThing())).

To fix this, we need to handle the autoclosure case specially. When we find an autoclosure in the innermost wrapper, we could take one of these approaches:

1. The memberwise initializer arg should be the wrapper type (Wrap) instead of the wrapped type (Int), so N would then be memberwise initialized as N(number: Wrap(wrappedValue: computeThing())), or
2. The memberwise initializer arg should be an autoclosed version of the wrapped type (@autoclosure () -> Int), so N would then be memberwise initialized as N(number: computeThing()).

I think approach #2 is better and will work on a PR with this approach.

In addition, if we find an autoclosure in a non-innermost wrapper, I think we should make the memberwise initializer take an arg of the wrapper type.

@ole
Copy link
Contributor Author

ole commented Jul 26, 2019

@roop Great analysis, thanks for getting to the bottom of this.

@roop
Copy link
Mannequin

roop mannequin commented Aug 9, 2019

Thanks, @ole.

PR opened: #26572

@ole
Copy link
Contributor Author

ole commented Aug 9, 2019

@roop Thanks so much for the PR.

@theblixguy
Copy link
Collaborator

@roop are you still working on this?

@roop
Copy link
Mannequin

roop mannequin commented Nov 7, 2019

@theblixguy Yes. I'll update the status in the PR in a couple of days and tag you.

@ole
Copy link
Contributor Author

ole commented Mar 23, 2020

Confirmed with DEVELOPMENT-SNAPSHOT-2020-03-22-a that @roop and @DougGregor fixed this with #30537 Thank you!

@swift-ci swift-ci transferred this issue from apple/swift-issues Apr 25, 2022
This issue was closed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug A deviation from expected or documented behavior. Also: expected but undesirable behavior. compiler The Swift compiler in itself property wrappers Feature: property wrappers
Projects
None yet
Development

No branches or pull requests

3 participants