Anonymous closure arguments vs varargs


(Slava Pestov) #1

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


(Tino) #2

Hi there,

I think it would be better if we permitted an implicit conversion between (T…) -> () and ([T]) -> ()

There has been a proposal to replace the "…" with a "variadic"-annotation (on arrays, or even on all types that can be expressed as arrays): https://github.com/Haravikk/swift-evolution/blob/a13dc03d6a8c76b25a30710d70cbadc1eb31b3cd/proposals/nnnn-variadics-as-attribute.md
The idea to accept array literals wherever a set, array etc. is expected came up in the discussion as well.
Interoperability with C has not been part of the debate, but besides that, imho it should be possible to get rid of varargs completely.

Imho the whole thread didn't receive the treatment it deserved because it happened in a very busy timeframe, but I wanted to asked the original author if he wants to continue working on the idea.

Afaics, the issue you found would be directly affected by the change in question, so I'm curious about your opinion on it.

Best regards,
Tino


(Douglas Gregor) #3

Hi Slava,

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

···

On Jan 4, 2017, at 6:28 PM, Slava Pestov via swift-dev <swift-dev@swift.org> wrote:


(Slava Pestov) #4

Hi there,

I think it would be better if we permitted an implicit conversion between (T…) -> () and ([T]) -> ()

There has been a proposal to replace the "…" with a "variadic"-annotation (on arrays, or even on all types that can be expressed as arrays): https://github.com/Haravikk/swift-evolution/blob/a13dc03d6a8c76b25a30710d70cbadc1eb31b3cd/proposals/nnnn-variadics-as-attribute.md

This sounds like a cosmetic proposal that doesn’t change semantics, so I don’t think it’s directly related to the change I’m proposing.

Slava

···

On Jan 5, 2017, at 1:05 AM, Tino Heth <2th@gmx.de> wrote:

The idea to accept array literals wherever a set, array etc. is expected came up in the discussion as well.
Interoperability with C has not been part of the debate, but besides that, imho it should be possible to get rid of varargs completely.

Imho the whole thread didn't receive the treatment it deserved because it happened in a very busy timeframe, but I wanted to asked the original author if he wants to continue working on the idea.

Afaics, the issue you found would be directly affected by the change in question, so I'm curious about your opinion on it.

Best regards,
Tino


(Tino) #5

This sounds like a cosmetic proposal that doesn’t change semantics, so I don’t think it’s directly related to the change I’m proposing.

Well, you're the expert here, and I haven't looked at the compiler source at all — but as I understand your first message, there are special vararg-types ("Int…"), and those would go away completely with Haravikks proposal.