As part of the @always(inline) discussion about controlling the optimizer's inlining, the idea of formalizing the underscored @_alwaysEmitIntoClient came up. Separately, the Embedded Swift Linkage model discussion discussed cases where treating everything as inlinable everywhere causes problems.
Here is a proposal to formalize the behavior of @_alwaysEmitIntoClient and also its opposite, @_neverEmitIntoClient. The full proposal text is here, but the specific proposal can be summed up as introducing two modifiers on the existing @inlinable attribute:
@inlinable(only): means that the function definition is inlinable and can only be used by inlining. Practically speaking, this means that a client has must emit its own definition of the function in order to use it, because the defining module does not emit a copy as a public, callable symbol. This spelling formalizes @_alwaysEmitIntoClient.
@inlinable(never): means that the function definition is never available to callers, even if the compiler options (aggressive cross-module optimization, Embedded Swift, etc.) would make it so by default. The defining module will emit a public, callable symbol that the client can use. At the time of this writing, the Swift main branch provides this behavior with the @_neverEmitIntoClient attribute.
I Googled the difference between the @inline(never) and the underscored @_neverEmitIntoClient that this would rename, and this seems like a good explanation of the distinction (and I’ve renamed the attribute to match the pitch for clarity’s sake):
Example scenario
Imagine you have a private helper function _expensiveCalculation() inside a library.
// A private helper function
@inline(never) // Prevents the local compiler from inlining it to keep binary size down
@inlinable(never) // Ensures the implementation is not copied into the client
internal func _expensiveCalculation() -> Int {
// A long, complex function body
return ...
}
// An @inlinable function that can be inlined into a client
@inlinable
public func publicApi() -> Int {
// Calls the internal helper function
return _expensiveCalculation()
}
In this scenario:
The @inlinable attribute on publicApi() allows its implementation to be inlined into a client's code.
However, the call to _expensiveCalculation() will not be inlined because of the @inlinable(never) attribute.
Instead, the client's binary will contain a call to the external _expensiveCalculation() symbol in the library.
The @inline(never) is redundant here for cross-module calls, but it could further prevent the compiler from inlining it even within the library itself.
I too think it would be a mistake to have the attributes @inlinable(...) and @inline(...) be spelled so similarly. The discussion in the thread on @inline(always) had mention of @_alwaysEmitIntoClient being somewhat orthogonal to @inline, but the pitched spellings here feel like a 5° difference than a 90° one—so close that they're almost parallel if you squint at them.
I'd rather we use a name that conveys the notion of "this will never have an ABI entry point" vs. "this will always and only have an ABI entry point". @Andrew_Trick 's suggestions above seem like a reasonable alternative.
I’ll counterpropose a syntax I’ve brought up a few places before for @inlinable(always): @abi(none). Since this attribute only affects the ABI, adding it to the @abi attribute keeps related concerns syntactically similar. It also naturally conveys to the reader that it can be safely ignored if you’re not trying to understand the ABI of a module. And it’s hopefully clear that it is incompatible with the @abi(func foo() syntax that defines a different ABI name for the symbol (since there is no actual ABI being produced).
The motivation for @inlinable(never)seems to be based around three use cases:
Allowing external linking. This seems better served by other language/ecosystem features, like SwiftPM/dynamic linking, public symbols, and @c @implementation. I’m don’t see how these existing features are insufficient to cover this use-case.
Stabilizing the public ABI across code changes. This feels better placed at the build system level (like we have today with library evolution). Manually identifying places in your code that need to be marked as non-inlinable externally seems like a recipe for extremely fragile builds. Maintaining ABI stability using library evolution mode is already quite challenging, and having to manually spell out the constraints would make it even more complex.
Build performance hacks. This use case feels very clearly like the responsibility of the build system to configure. For example, it could maintain a memory of which function implementation changes led to the longest incremental compiles and use that to configure the compilation process to temporarily stabilize those functions to speed up future compilation. This kind of thing would not need any sort of source-level feature to activate it and would therefore keep the source code focused on implementation rather than configuration.
Finally, the table at the bottom of the document does a good job of explaining the different combinations of @inline and @inlinable, but it does not explain when each of those combinations would be applicable. I think if we had a better idea of the value of each one, it could inform a different syntax that leaves fewer different behaviors for users of the language to understand while preserving the most useful options.