[swift-dev] Anonymous closure arguments vs varargs

Douglas Gregor dgregor at apple.com
Fri Jan 6 13:06:25 CST 2017


Hi Slava,

> On Jan 4, 2017, at 6:28 PM, Slava Pestov via swift-dev <swift-dev at swift.org> wrote:
> 
> Hi all,
> 
> In Swift 3.0, the following examples both typecheck:
> 
> let fn1: ([Int]) -> () = {
>  let _: [Int] = $0
> }
> 
> let fn2: (Int...) -> () = {
>  let _: [Int] = $0
> }
> 
> This stopped working due to a regression in master so I'm looking at fixing it.

Whoops, thanks!

> 
> While investigating this, I noticed that this variant with two parameters doesn’t work in either release:
> 
> let fn3: (Int, Int...) -> () = { // cannot convert value of type '(_, _) -> ()' to specified type '(Int, Int...) -> ()'
>  let _: Int = $0
>  let _: [Int] = $1
> }
> 
> The diagnostic doesn’t make sense, which suggests there’s a deeper underlying problem.

Yeah.

> 
> Indeed, the reason the ‘fn2’ example works in Swift 3.0 is because we bind $0 to the single-element tuple type (Int…), which admits an implicit conversion to [Int]. The closure literal gets the type ((Int…)) -> () — note the extra pair of parentheses here. This works mostly on accident. For example if we bind $0 to a generic parameter, SILGen blows up:
> 
> func id<T>(t: T) -> T {
>  return t
> }
> 
> let fn4: (Int...) -> () = {
>  id(t: $0) // segfault here
> }

Hmm. We’re not modeling the difference between the type of the parameter as seen in the body of the closure and the function input type as separate things.

> I think it would be better if we permitted an implicit conversion between (T…) -> () and ([T]) -> (), or more precisely, erase varargs when matching function arguments in matchFunctionTypes() if we’re performing a Subtype conversion or higher.
> 
> After adding this to CSSimplify, I notice that in fn2, $0 now gets type [Int] and the closure has type ([Int]) -> (), wrapped in a FunctionConversionExpr converting to (Int…) -> (), which ends up being a no-op since varargs are erased in SILGen. Also, fn3 and fn4 start working; in fn3, $1 gets type [Int], and in fn4, we also correctly bind the generic parameter to [Int]. I think this is a better situation overall. Values should not have types containing vararg tuples, and we should prevent these types from showing up in the type system as much as possible.
> 
> However, this more general conversion rule also means the following is allowed, whereas it did not typecheck before:
> 
> func varargToArray<T>(fn: @escaping (T...) -> ()) -> ([T]) -> () {
>  return fn
> }
> 
> func arrayToVararg<T>(fn: @escaping ([T]) -> ()) -> (T...) -> () {
>  return fn
> }
> 
> This is essentially what Dollar.swift was doing, but they were using a closure literal to achieve it.
> 
> At this time, the conversion can be performed without thunking, but if varargs ever get a different representation, we can still thunk the conversion like we do for re-abstraction, optionality changes, existential erasure in function types, etc.
> 
> Does anyone foresee any problems with this approach? We could also conceivably limit this conversion to closure literals only, and not general subtype conversions.

Personally, I would prefer to limit this conversion to closure literals only. It’s a narrower change, and it avoids having to build a thunk if we do improve the representation of varargs.

	- Doug



More information about the swift-dev mailing list