As somebody who implemented SE-0307 I'd like to share a couple of insights about implicit conversions when it comes to a type-checker performance.
All of the following are empirically proven to lead to a bad performance:
- Allowing (pure) bi-directional conversions. In case of Double/CGFloat it led to ambiguities even in relatively simple expressions.
- Allowing multiple conversions in the same position e.g. converting from Double to CGFloat and back to Double.
- Allowing conversions in argument positions, especially for homogeneous operators (which allow of a sudden became heterogeneous) because it leads to unexpected results. Double/CGFloat managed to (somewhat) work around that by preferring Double over CGFloat.
I'm not going to re-iterate what @hborla and @codafi already said (and with which I agree), just want to add that while working on SE-0307 I did consider possible ways to implement that particular conversion in a more general way that could be extended to widening of integers (this is also discussed in pitch for Double/CGFloat conversion as possible future direction) but the problem is that even widening has big performance implications although it's scoped down to just a handful of types.
I think if we were to consider a general feature here we'd have to put so many restrictions on its syntactic use that it couldn't be called "general" in a normal sense and wouldn't be worth the complexity.
Now let's consider one possible solution as implemented in the associated PR. The idea is to add a non-failable initializer for each possible conversion type could have e.g.
extension UnsafeRawPointer {
init(implicit: UnsafeMutableRawPointer) { ... }
}
Could be used to implement uni-directional conversion from UnsafeMutableRawPointer to UnsafeRawPointer. Without bike-shedding to much on the syntax let's consider what the semantics might be:
- Any struct, class, enum (or nominal for that matter) should be convertible to other type if it has an initializer in form of
init(implicit: T)whereTis the "from" conversion type.
Which means that every Conversion (of any type e.g. argument, operator argument, regular) or Subtype relationship in the type system now has to check whether right-hand side has an initializer that accepts left-hand type as an argument. This new rule could be expressed in a following set of constraints:
Starting from:
$From <convertible to> $To
Simplifies into:
$To[.member == init(implicit:)] == $Init
($From) -> $To <applicable function> $Init
Note that after lookup $Init could be bound to a number of choices with different types of arguments (important - including failable initializers) if $To is convertible to N different types.
After $Init is determined it's possible to simplify ($From) -> $To <applicable function> $Init into:
$From <argument conversion> $Init.$Arg0
$To <function result> $Init.$Result
Further simplification results in yet another [.member == init(implicit:)] ... check for $Init.$Arg0 and $From types if $To has multiple overloads of init(implicit:).
It could be reasonable to disable followup checks in "implicit conversion" context but that would restrict the generality of the feature.
This new rule impacts all expressions in the language non-trivially but having to perform at least one additional member lookup per any kind of conversion, even if a type doesn't support implicit conversions. It could be argued that we could use a declaration attribute to mark some of the types as @implicitlyConvertible to avoid having to pay the extra cost in the type-checker, but if we did that for some commonly used types e.g. to support widening conversions it would still impact a non-trivial amount of code and retroactively adding such an annotation might not be possible.
My suggestion would be to consider merit of an uni-directional conversion from UnsafeMutableRawPointer to UnsafeRawPointer just like we did Double/CGFloat and discuss general implicit conversions separately based on the knowledge gained from it and previous conversions.