Skip to content

[Property wrappers] Fix handling of @autoclosure in init(wrappedValue:) #30537

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

Merged

Conversation

DougGregor
Copy link
Member

Note: this pull request picks up #26572 and fixes
a few merge conflicts. All thanks go to @roop

If the ‘wrappedValue:’ parameter is an escaping autoclosure, and a
struct property is marked with that property wrapper, the memberwise
initializer of the struct is now synthesized with an escaping
autoclosure for that property.

Resolves SR-10950.

Given a Lazy property wrapper:

@propertyWrapper
enum Lazy<Value> {
  …
  init(wrappedValue: @autoclosure @escaping () -> Value) {
    …
  }
  …

and a struct using the property wrapper

struct S {
  @Lazy var n: Int = computeInt()
}

the memberwise initializer for S is currently synthesized as:

init(n: Int = computeInt())

This is incorrect because if we create S like var s1 = S() or like var s2 = S(n: computeInt()), computeInt() will get evaluated at the time of creation of S, which

  • is not consistent with the @autoclosure specification
  • makes @Lazy not really initialize things lazily

To fix this, this PR makes the memberwise initializer get generated as:

init(n: @autoclosure @escaping () -> Int = computeInt())

Implementation details:

  1. Changed the type of the opaque placeholder (lib/Sema/TypeCheckStorage.cpp): PropertyWrapperBackingPropertyInfo.underlyingValue is an opaque expression that serves as a placeholder for what should be used when initializing a struct using a property wrapper. For the above scenario, the type of this expression is changed to () -> Int (rather than Int previously).
  2. Changed the signature of the memberwise initializer (lib/Sema/CodeSynthesis.cpp): The argument type is changed to a closure and autoclosure-ness is set.
  3. Handled default values provided with an “=“ in the wrapper (lib/SIL/TypeLowering.cpp, lib/SILGen/SILGenConstructor.cpp, lib/SILGen/SILGenFunction.cpp): The type is likewise changed to a closure.
  4. Because of (1), the opaque underlyingValue is of a closure type. I should ideally be passing this as the ‘wrappedValue:’ parameter, but we can’t pass a closure to an autoclosure arg. So, instead, I pass an AutoClosureExpr of a CallExpr of a call to this closure. (lib/Sema/TypeCheckPropertyWrapper.cpp)
  5. Because of (4), I now have an extra CallExpr in the expression tree which causes problems in SILGen. So, while traversing the expression tree, I skip the AutoClosureExpr and CallExpr. (lib/SILGen/SILGenExpr.cpp)

roop and others added 6 commits March 20, 2020 12:20
If the 'wrappedValue:' parameter is an escaping autoclosure, and a
struct property is marked with that property wrapper, the memberwise
initializer of the struct is now synthesized with an escaping
autoclosure for that property.
When assigning to a wrapped variable in 'init()', if the property wrapper's
'wrappedValue:' parameter is an escaping autoclosure, the initializer and
setter have incompatible types -- the setter takes a value, and initializer
takes a closure returning that value.

An assign_by_wrapper with incompatible types causes SIL verification to fail.

This commit makes the SIL not use assign_by_wrapper in such cases, and use the
setter directly, resulting in a definitive initialization (DI) error instead.

    struct S {
      @lazy var n: Int // Lazy.init(wrappedValue: @autoclosure ... )
      init() {
        n = 1 // error: 'self' used before all stored properties are initialized
      }
    }
GetterSetterComponent::hasPropertyWrapper() now checks for more
stuff than just whether there's a property wrapper or not.

It's therefore renamed to canRewriteSetAsPropertyWrapperInit().
@DougGregor
Copy link
Member Author

@swift-ci please smoke test

@DougGregor
Copy link
Member Author

The Linux failure looks spurious, but I've found another issue in testing that I'd like to address before merging.

Fixes the test case from rdar://problem/58986940.
@DougGregor
Copy link
Member Author

@swift-ci please smoke test

@DougGregor
Copy link
Member Author

@swift-ci please test source compatibility

@DougGregor
Copy link
Member Author

@swift-ci please test source compatibility

@DougGregor
Copy link
Member Author

@swift-ci please test Linux

@swift-ci
Copy link
Contributor

Build failed
Swift Test Linux Platform
Git Sha - af1abea

// created when we have a property wrapper's 'init(wrappedValue:)'
// taking an autoclosure argument.
if (auto ace = dyn_cast<AutoClosureExpr>(e)) {
if (auto ce = dyn_cast<CallExpr>(ace->getSingleExpressionBody())) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems really, really brittle. Is there a way we can keep this in sync with the synthesis code?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't come up with a good way, no. In general we really need a better way to pull out the initial value expression from the type-checked AST for the initialization of the backing storage, because that code also needed too much tweaking. Maybe there's some better way to grab the expression at the right point in CSApply.cpp so we can save it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants