Adding a new semantics attribute: must_specialize

What?

I'm proposing that we add a new semantics attribute named must_specialize. This attribute will force a generic function to be specialized. If the function cannot be specialized, a compile-time error will be emitted.

I propose that a function marked with must_specialize must either be called from a non-generic function or a function also marked with must_specialize. If a must_specialize function is called from a generic function without this attribute, then the attribute will be applied to the caller implicitly, and the same rules will apply to the caller.

Why?

This is a solution to the problem discussed in this forum post. We need a way to specialize C++ function templates, and this would fix that in a very general way. The desired behavior, to look through callers and find concrete generic arguments for C++ function templates, can be achieved by simply applying the must_specialize attribute to all function templates we import.

This also potentially gives non-C++ users more control over the specialization of their generic functions and may offer better performance in certain cases.

Future.

In the future, it may make sense to turn this into a public attribute. This is not what I am proposing here, though. This is only for internal use, specifically in C++ interop.

I have implemented this proposal locally; I will be putting it up for review shortly.

6 Likes

If this is an internal only feature, you might want to name it _must_specialize.

Semantics annotations generally are internal-only features, as far as I’m aware.

I’m not sure why the restriction here. If I have a function generic over StringProtocol, why should I not be allowed to call a must_specialize numeric function with concrete Int arguments? I think the restrictions here that make sense are the ones that naturally fall out when you say that compilation will fail if the function can’t be specialized.

2 Likes

There are two different semantics described by this paragraph:

  • generic callers must be explicitly marked must_specialize.
  • must_specialize implicitly propagates up the call stack, so generic callers need not be explicitly marked.

Which is it?

How does this proposed semantic interact with ABI stability?

3 Likes

Very cool, I'm looking forward to this Zoe! This seems like it could be generally useful functionality for high performance workloads - allowing people to get predictable performance for specific important cases.

1 Like

At first blush, this seems to me like it'd be insufficient for C++ interop, since C++ templates can expand differently in arbitrary ways that can't really be expressed as Swift generics. Although we could perhaps still let Swift generics syntax be used for referring to imported C++ template instantiations, we'd still have to perform that instantiation given the concrete type arguments, and from there treat the instantiation as effectively a new independent declaration, rather than an instance of a common generic declaration. Even with a "must specialize" attribute you could apply to generic Swift declarations, you wouldn't be able to refer to arbitrary C++ templates with normal generic semantics either, because it seems like we would need to also re-instantiate the Swift caller like a template in order to properly re-instantiate and re-type-check the new C++ template instance.

2 Likes

Yes, you're right. The restriction you're talking about makes much more sense.

Note: using this attribute for something in the stdlib which may be called by a user is very dangerous. It might cause compile-time errors, depending on how it's used, or really hurt code-size.

Well, it really is both. Another way to think about it is, "generic callers must be explicitly marked must_specialize" and "if this is not the case, the compiler will fix it." It's mostly an implementation detail. I also thought it would be a clear way to articulate the idea that the restrictions that apply to functions marked as must_specialize also apply to their callers and their callers callers, etc. all the way up.

This is a really good question. The good news is that this should not affect ABI stability. Functions marked as must_specialize must be specialized, so a new function will be created. Additionally, after all the specializations of the must_specailize function have been created, the function will be deleted. It cannot be used anymore because that would violate its restrictions. This implicitly means that any functions marked with must_specialize must be internal/private functions. They cannot be exposed publically, and therefore, should not affect the ABI. Note: this rule applies to all functions marked as must_specialize, so, as described above, these restrictions will also be applied to any "generic" callers.

Looking back at my post, I didn't call out the "mustn't be public" rule explicitly. That's my bad, I should have stated that.

This is getting into some of the complicated problems discussed in the linked forum post. First, I want to be clear that I am only discussing function templates. Specializing, extending, and type-checking class templates are going to have a whole host of complications that we only began to discuss in that forum post. I think this is not the correct place to discuss those problems.

Second, let me clarify that this attribute will have nothing to do with the functions that we are able to import. At this point, the compiler will already have function declarations which may be called. The problem I'm trying to solve here is, how do we find the concrete types that are used to specialize those (already imported) declarations.

because it seems like we would need to also re-instantiate the Swift caller like a template in order to properly re-instantiate and re-type-check the new C++ template instance.

I think you are correct, and I think that's actually exactly what this attribute is doing. However, we don't need to re-type-check the Swift function that is calling the C++ template instance. Once we specialize the Swift function, then we will have all the concrete type substitutions. We can simply give Clang both the type substitutions and the template we want to instantiate and it will do the rest (create the new function and type check it). This is the same way we currently instantiate C++ function templates, it's just a matter of specializing the Swift functions first so that we know we'll have concrete template arguments that we can provide the C++ function template.

I think the problem you're getting at is the problem we spent so much time discussing in the other forum post: doing what I'm suggesting here means we lose the beauty of Swift generics. In other words, for C++ function templates, we won't have the ability to constrain generic arguments properly. I.e., if there was a C++ function template that added two template parameters and a generic Swift function which called the C++ function template, the Swift generic types would not be required to conform to AdditiveArithmetic. This means, if the substituted types weren't addable, then we'd get a compile-time error from Clang complaining about it. This is substantially worse because it introduces the "10 calls deep" instantiation errors we all know and hate.

Unfortunately, I think this is a bit of an inherent problem with the C++ language. As Dave and I discussed in the other forum post, we can probably "solve" this to a certain degree with class templates. But I'm not sure there's a way for us to fix this problem regarding function templates. Even if we did want to somehow automatically generate protocols to constrain C++ function template type arguments (which may actually be possible), I think that would be something that would require lots of work, and would probably better be done as an improvement down the road. What I'm proposing is a fairly simple solution, while maybe not ideal, I think it will get the job done quickly. We can always change it in the future.

@dabrahams what do you think?

1 Like

If I were to try to come up with an analogous approach to what we discussed for C++ class templates, it would need a new language feature: the ability to write a constraint in Swift for the C++ function template, describing the expected relationships among its generic parameters. Without such a constraint, Swift code would only know what is inherent in the bare C++ signature.

"10 calls deep" instantiation errors are going to happen one way or another if C++ is involved, but my goal in proposing something like this was to quarantine them to C++ code and not have them pointing to more than one place in the Swift code.

1 Like

I'm not sure that limiting the scope to function templates is still sufficient to avoid re-type-checking, since function templates alone can still end up with type signatures that vary in ways that can't be represented as Swift generics, like template<typename T> std::make_unsigned<T>::type foo(T). I also think that some amount of discussion of the model is unavoidable here, because if the intent of the attribute is "allow C++ templates to be instantiated", then that's very different from, say, Chris's suggestion of using this as an optimizer control. We may have to accomodate a different language model to coexist with C++ templates, but a performance control OTOH should not change language semantics so deeply.

1 Like

Yes, this is exactly what we need. I'm just not sure how to create this. Maybe we could have Clang look through every operation for a given argument of type T. But, as I said above, that sounds like a lot of work. And it might just be best to implement the simple thing first, so people can start using function templates in more complicated ways.

I'd also like to point out that there's no reason people can't manually constrain their generic arguments with protocols. We can simply tell people "please create protocols that describe the expected constrains for a given function template parameter type." There aren't any guarantees with this method, but it's better than nothing.

Follow-up questions:

  • I assume must_specialize applies to debug builds as well as release (it has to, for the use you are targeting). I think this is fine, but it's a departure from the similarly named inline(__always), for instance.
  • You say any uses must be internal / private, but what about internal @usableFromInline? @_alwaysEmitIntoClient? I don't think there's a big problem here, I just want to pin down the rules.

It is a bug that inline(__always) doesn't always inline IMO (given it doesn't inline at -Onone).

So, my thinking is that this would happen before any inlining so the semantics would be preserved. However, preserving semantics through inlining is a bigger problem (luckily not relevant here) that we should probably find a way to sort out at some point.

1 Like

Here is my implementation: Add semantics attribute: must_specialize. by zoecarver · Pull Request #36425 · apple/swift · GitHub

I tried to incorporate the feedback from this thread, so as per @xwu's suggestion, a "must_specialize" function now must only be called with a concrete substitution map, not necessarily by a non-generic function. And, I added a specific error for public functions marked as "must_specialize" (and their callers).

I‘m probably a bit late to the party, but one more thing to think about: shouldn’t the implicit internal/private restriction only apply to dynamic libs? If it is known that the defining module will be statically linked, I don’t see a problem with public must_specialized methods/types.