True Generic Specialization with annotation?

Hi Guys, the following example:

let a:Int = 2;
let b:Float=2.0
let c:String="2.0"



func typeOfVal(_ a:Int)->Void{
    print(type(of:a))
}

func typeOfVal(_ a:Float)->Void{
    print("Float")
}


func typeOfVal<T>(_ a:T)->Void{
    print("Generic")
}

func typeOfValIndirect<T>(_ a:T)->Void{
    typeOfVal(a)
}

typeOfVal(a)
typeOfVal(b)
typeOfVal(c)
typeOfValIndirect(a)
typeOfValIndirect(b)
typeOfValIndirect(c)

When executing this on repl.it/languages/swift I get the following:

 swiftc -o main main.swift
 ./main
Int
Float
Generic
Generic
Generic
Generic

While the first three calls are ok, the latter are not.

I know that the designers of swift had their reasons to support only local specialization, but why we just couldn't override this behavior in these cases which certainly causes specialization to be a bit slower but gives us the desired behavior?

What about an annotation like @globalSpecialization over the typeOfVal functions?

See also: Compile-Time Generic Specilization

Case (1) Let's say we have this in a module A. What'd happen if I do this?

// In module B
import A

func typeOfVal<T: StringProtocol>(_ a: T) { print("StringProtocol") }

func foo() {
  typeOfValIndirect("String")
}

I beckon you'd want them to print StringProtocol?


Case (2) Now, to have a little more fun, I then add this:

import B
protocol Bar {}
extension String: Bar {}
func typeOfVal<T: Bar>(_: T) { ... }

Now foo should fail to compile, no?


Case (3) Or this:

import B
func typeOfVal(_: String) { ... }

Now foo should print String instead. Not sure if that's what module A have in mind when implementing it.


Case (4) That wasn't fun enough, let's have two function arguments:

func typeOfVals<A, B>(_: A, _: B) {
  print("Generic Generic")
}
func typeOfValsIndirect<A, B>(_: A, _: B) { ... }

and repeat all of our experiments.


Do you have some implementation mechanisms in mind to handle these cases? Insofar, I can only think of requiring all callers, callers' callers, and so on to be @inlinable.

Why @inlinable?

We don't need to see actually the body of a function, but we are required to see the specializations/overloads of functions in other modules with this annotation, even when they are in another lib.

Yes, I would require to annotate all callee's declarations. What is so bad about that, bad performance?
I even assume compile time erroring if a call of a method is ambiguous, but overlapping methods should be allowed.

I thought Swift already had the @_specialize annotation for it, but it doesn't seem to work with separate overloads, I want an analog of this.

As you said, @_specialize works with the same generic function. So it guarantees the same behaviour whether the specialization is used or not. So it's fine as a compilation hint as it would only affect performance.

If we want to use two different functions, then the behavioural difference could become visible. In that case, it's important that @globalSpecialization is not a hint, but a guarantee, and that is a lot trickier to pull off.

How do you expect a compiled binary to handle that? Even for as?, we do know what conformance we're checking for is at compile time. I'm not sure how you can make it work with case (1) for non-inlinable code (numbers added for easy reference).

You mean callers'? Not much really, either way. If one is willing to use the feature, I'm sure they already consider the performance implication. I just want to make sure we consider the implications of such feature.


Also, it'd be quite baffling it adding a typeOfEval overload causes a random function foo to fail to compile (Case (2)).

Yes, it should be semantic guarantee, I thought this is possible too with @_specialize when I read it correctly it should be possible to make the signature visible to end users and this is likewise required for correct specialization.

The compiled binary should store this kind of overloaded set in a demangled form. When signatures are public, then any dynamic linked lib must provide these functions with the same signature in demangled form.

No, callees must be demangled (callees can be indirectly callers too). The caller can also be demangled, but I would leave the compiler the decision if dispatching occurs statically or dynamically as far as I understand Swift's Generics model.

Yet I understand what you mean, I wasn't even aware that Swift supports cyclic imports, but yes it should throw a compiler error. And yes, adding an overload can lead to error, what is so strange about that?
These overloads overlap, ambiguity is implied.

Case 3, yes. Because of String is the best match, the nearest neighbor.

Case 4. Introducing more than one argument will increase ambiguity as more sets overlap.
Just to say, there exists an overload resolution in C++ and in D to handle these cases, even Rust want to utilize full specialization, but as it seems, they can't because of their trouble with lifetime erasure.

You presented extreme cases, here and I don't want to propose it as a sole replacement of current dispatching.

Edit: Correctly thinking about, we may don't demangling, however we need to get access to all methods signature and to select methods out of multi function.