KeyPath as function inside property wrapper doesn't compile in 5.2 (fine in 5.3)

I'm having a strange compile issue in Xcode 11/Swift 5.2 which compiles fine in Xcode 12/Swift 5.3 - I'm unsure if this is due to a bug in the previous version of Swift so was wondering if somebody could help because the compiler error is a bit...cryptic.

I'm just experimenting with a small high-level validations library and am trying to implement a property wrapper. I have a generic type, ValidatorOf<Value, Error> which can define a validator on a particular type:

I have a wrapper type, which can be used as a property wrapper, called Validating<T> which wraps an underlying value of type T and also a validator of type ValidatorOf<T. String> (the second generic parameter is just the error type):

I have a high-level validator that allows you to define a ValidatorOf<Value> against a child of Value of type T, by providing a transform function (Value) -> T, defined as:

extension ValidatorOf {
    static func its<T>(_ transform: @escaping (Value) -> T, _ validator: ValidatorOf<T, Error>) -> Self {
        validator.pullback(transform)
    }
}

The pullback function can be seen in the ValidatorOf.swift file above.

So, on to the issue...first to demonstrate how the property wrapper works - I can define a validated string on a struct as follows:

struct Foo {
    @Validating(.beginsWith("foo"))
    var someString: String
}

This works fine. The problem arises when using the its validator. The following works in both Swift 5.2 and 5.3:

struct Foo {
    @Validating(.its({ $0.count }, .isAtLeast(6)))
    var password: String
}

The problem arises with the key-path form of the above:

struct Foo {
    @Validating(.its(\.count, .isAtLeast(6)))
    var password: String
}

In Swift 5.3 this works fine, but in Swift 5.2 I get the errors:

Struct declaration cannot close over value '$0' defined in outer scope
Struct declaration cannot close over value '$kp$' defined in outer scope

Build output with this error can be seen here:

Thanks!

Here's a much smaller reproducer:

struct GenericStruct<Value, Error> {
  static func function<T>(_ transform: @escaping (Value) -> T) -> Self {
      fatalError()
  }
}

@propertyWrapper
struct Wrapper<Value> {
  var wrappedValue: Value {
      fatalError()
  }
    
  init(wrappedValue: Value, _ struct: GenericStruct<Value, String>) {
     fatalError()
  }
}

struct UsesWrapper {
  @Wrapper(.function(\.count))
  var property: String = ""
}

The error is probably a result of a bug somewhere in capture analysis which got fixed in 5.3, which is why your code compiles in Xcode 12 beta.

I think you can workaround the problem by doing it manually:

static func its<T>(_ transform: KeyPath<Value, T>, _ validator: ValidatorOf<T, Error>) -> Self {
  validator.pullback { $0[keyPath: keyPath] }
}

I forgot to mention - the $0 and $kp$ parameter labels come from the implicit closures that are created by the compiler to turn the KeyPath literal into a function. You can read more about it in the proposal that added this feature.

1 Like

@suyashsrijan thank you very much for taking the time to investigate and reproducing in isolation. Glad to hear it's a bug and not me doing something crazy. I'll give your workaround a try.

EDIT - workaround is working nicely, thank you.
https://github.com/lukeredpath/swift-validations/commit/4e2e355c272a4471ffbb11d4ad56e397c7b46606

1 Like