Lifetime dependency annotations and function subtyping

When a function returns a non-escapable type that copies the lifetime dependencies of an input, the copy lifetime annotation is used:

struct NE: ~Escapable { }

@_lifetime(copy ne)
func copies(_ ne: borrowing NE) -> NE { return copy ne }

Such a function that returns a copy will still type-check, even if the lifetime annotation is replaced with borrow ne or &ne (if the parameter is changed to be taken as inout):

@_lifetime(borrow ne)
func stillCopies(_ ne: borrowing NE) -> NE { 
    return copy ne
    // OR, equivalently:
    // return copies(ne)
}

@_lifetime(&ne)
func stillCopies2(_ ne: inout NE) -> NE { 
    return copy ne
}

This makes sense—creating a scoped dependency on a borrow of ne necessarily implies that anything ne in turn depends on (i.e. the dependencies get copied above) is still alive as well.

This behavior led me to believe that there should be a subtyping relation between such function types, where the type of copies would be a subtype of the type of stillCopies, since any function with a copied dependence could be used as one with a scoped dependence. However, this is not the case:

// does NOT typecheck
// error: cannot convert value of type '@_lifetime(copy 0) (borrowing NE) -> NE' to specified type '@_lifetime(borrow ne) (_ ne: NE) -> NE'
let clos : @_lifetime(borrow ne) (_ ne: NE) -> NE = copies;

A similar error arises when attempting to conform to a protocol written with a @_lifetime(borrow ne) annotation using a conformance annotated as @_lifetime(copy ne). Note that these are positions where sub-lifetime annotations are permitted (for instance, a protocol annotated as @_lifetime(copy a, copy b) may have a conformance annotated with just @_lifetime(copy a)

Am I incorrect in thinking that this subtyping relationship should hold? Or would this be sound, and my above observations are merely limitations of the current implementation?

2 Likes

Great question, I had the same confusion! I said (as part of some other discussion, about what witnesses were valid for a protocol with dependency annotations)

immortal <: borrow self <: copy self, but not the other way around, copy self is "weaker" guarantee than borrow self , generic code may want to consume P and still use the Ref, but copy self won't allow that. (this is isn't accepted by compiler today but i do think it could be made to work?)

@aidan-hall said (hope it's ok to copy his message :grin: )

We did consider allowing conversion between copy and borrow when adding lifetime checking for protocol conformances, and just didn't at the time because it wasn't necessary to support any conformances in existing code.
(link to internal "allow conversion (contravariance) from lifetime(copy) to lifetime(borrow) for function types (protocol/closure)")

The conversion rules as implemented by the compiler today are documented here (sorry if the wording is confusing):
swift/docs/ReferenceGuides/LifetimeAnnotation.md at main · swiftlang/swift · GitHub

So, afaik, yes this subtyping relationship is safe and could safely imply a subtyping relationship between function types, but no the compiler does not support it.

4 Likes

Oops this is backwards, should read immortal <: copy self <: borrow self I think... my whole answer seems transposed wrt function type conversion.