RFC: bridging peephole for "as" casts

So, there's a longstanding issue that we're planning to fix in Swift 4, and I want to both make sure that the plan is documented publicly and give people a chance to disagree with it.

A bridging conversion is a conversion between a Swift type and a foreign type (C / ObjC / whatever) which can represent the same set of values. For example, there are bridging conversions from Swift.String to ObjC's NSString and vice-versa. When there two-way conversions like this, we say that the Swift type is bridged to the foreign type.

Bridging conversions are performed for three reasons in Swift:

1. You can always request a bridging conversion with an unconditional "as" cast. For example, if myString is a String, you can convert it to NSString by writing "myString as NSString".

2. Certain bridging conversions can be introduced as implicit conversions. (This is perhaps a mistake.) For example, CFString and NSString are considered different types, but they will implicitly convert to each other.

3. Bridging conversions are done "behind the scenes" when using an imported declaration that has been given a type that does not match its original type. For example, an Objective-C method that returns an NSString will be imported as returning a String; Swift will implicitly apply a bridging conversion to the true return value in order to produce the String that the type system has promised.

Bridging conversions are not always desirable. First, they do impose some performance overhead which the user may not want. But they can also change semantics in unwanted ways. For example, in certain rare situations, the reference identity of an NSString return value is important — maybe it's actually a persistent NSMutableString which should be modified in-place, or maybe it's a subclass which carries additional information. A pair of bridging conversions from NSString to String and then back to NSString is likely to lose this reference identity. In the current representation, String can store an NSString reference, and if the String is bridged to NSString that reference will be used as the result; however, the bridging conversion from NSString does not directly store the original NSString in the String, but instead stores the result of invoking +copy on it, in an effort to protect against the original NSString being somehow mutable.

Bridging conversions arising from reasons #1 and #2 are avoidable, but bridging conversions arising from reason #3 currently cannot be eliminated without major inconvenience, such as writing a stub in Objective-C. This is unsatisfactory. At the same time, it is not valid for Swift to simply eliminate pairs of bridging conversions as a matter of course, precisely because those bridging conversions can be semantically important. We do not want optimization settings to be able to affect things as important as whether a particular NSString is mutable or not.

The proposal is to apply a guaranteed syntactic "peephole" to eliminate bridging conversions that arise from reason #3. Specifically:

  No bridging conversions will be performed if:
    - a call, property reference, or subscript reference is the immediate syntactic
      operand of an "as" cast to a type compatible with the foreign return, property,
      or subscript element type or
    - a call argument, right operand of an assignment to a property reference, or
      right operand of an assignment to a subscript reference is an "as" cast from a
      type compatible with the foreign parameter, property, or subscript element type.
  Two types are "compatible" if there is a simple subclass or class-protocol relationship
  between the underlying non-optional types.

We believe that this rule is easy and intuitive enough to understand that it will not cause substantial problems.

Thanks for writing this all down, John. Should returns also be included in this? That is:

override func someObjCFunction() -> String {
  return NSMutableString() as String
}

Hmm. We're not in a position to make this guarantee easily, because this isn't inherently a foreign method in the same way that an imported API is. Maybe we can find a way to promise that later? Or maybe we could just allow such overrides to use the foreign types instead of the native ones.

(and, having written that, it seems useful to show code examples for each of your cases.)

Sure.

This would avoid the bridging conversions through [View] on the return value of the getter:
  let subviews = view.subviews as NSArray

This would not:
  let subviews = view.subviews
  let nsSubviews = subviews as NSArray

This would avoid the bridging conversion through [CIFilter] on the argument to the setter:
  view.backgroundFilters = nsFilters as [CIFilter]

This would not:
  let filters = nsFilters as [CIFilter]
  view.backgroundFilters = filters

John.

···

On Jun 13, 2017, at 7:32 PM, Jordan Rose <jordan_rose@apple.com> wrote:

On Jun 13, 2017, at 16:11, John McCall via swift-dev <swift-dev@swift.org <mailto:swift-dev@swift.org>> wrote: