[swift-dev] Anonymous closure arguments vs varargs

Slava Pestov spestov at apple.com
Wed Jan 4 20:28:27 CST 2017


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.

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.

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
}

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.

Slava


More information about the swift-dev mailing list