Skip to content

Conversation

aschwaighofer
Copy link
Contributor

No description provided.

@allevato allevato self-assigned this Sep 22, 2025
@rjmccall rjmccall added the LSG Contains topics under the domain of the Language Steering Group label Sep 23, 2025
ensures that there is a public entry point to the `internal` level function but
does not ensure that the body of the function is available to external
modules. Therefore, it is an error to combine `@inline(always)` with a
`@usableFromInline` function as we cannot guaranteed that the function can
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
`@usableFromInline` function as we cannot guaranteed that the function can
`@usableFromInline` function as we cannot guarantee that the function can

Comment on lines +117 to +118
@inline(always)
func mightBeOverriden() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there ever a situation where a non-final class method would be able to be guaranteed inline? Should this be diagnosed as an error at the declaration site?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there ever a situation where a non-final class method would be able to be guaranteed inline?

In theory, yes. But we would be relying on "mandatory optimizations" doing function local type propagation and devirtualization.

func localDevirt() {
   let c = BaseClass()
   c.method() // c's type is know to be BaseClass therefore we can replace it by a "direct reference"
}

But, I think we should weight this against the intention that we want the method to be inlined in "most cases". Therefore, I think making a non-final method with @inline(always) an error is more in spirit with the desire for @inline(always) to be an optimization control.

Should this be diagnosed as an error at the declaration site?

Yes. Thank you for pointing this out. Thinking more about the intend of the attribute optimization control, it should be an error marking a non-final class method as @inline(always) because there is no way of "directly calling it" without semantically having to go through a v-table.

This is in contrast to the call to an enum/struct's method which can be called directly (via the struct/enum's type) or through a protocol conformance.

Comment on lines +194 to +196
This proposals takes the position to give `@inline(always)` the semantics of
`@inlineable` and provide an alternative spelling for the case when we desire
`@_alwaysEmitIntoClient` semantics: `@inline(only)`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Language Steering Group discussed this and we feel that the @inlinable semantics are likely the right choice for @inline(always), but that @_alwaysEmitIntoClient is more of an orthogonal concept, since it just means that the body will be emitted into the client object but not necessarily inlined at the call sites (even though the optimizer may choose to do so since it can see the body). So we're requesting that this form of the attribute be removed from the pitch.

See also Doug's comment in the thread: https://forums.swift.org/t/pitch-inline-always-attribute/82040/22

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay.

Comment on lines +274 to +277
As outlined earlier the attribute does not guarantee inlining or diagnose the
failure to inline when the function value is dynamic at a call site: a function
value is applied, or the function value is obtained via class method lookup or
protocol lookup.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This carve-out seems like it defeats some of the goals of the guarantee and associated diagnostic, since the proposal says in one place that we should emit a diagnostic when it's not guaranteed, but then it lists these exceptions. I'm trying to think of concrete cases that are left where we can decide that inlining isn't possible and would diagnose it.

The recursion case you mention above is one where inlining would definitely not be possible.

The protocol/concrete type case is one where it makes sense to not diagnose. For example,

protocol P { func someFunc() }
struct S: P {
  @inline(always) func someFunc() {}
}

In this case, if someFunc is called on an instance of the concrete type, we know we can inline that, but if it's called on a generic or existential constrained to P, we couldn't. That seems fine, and I don't think it makes sense to diagnose in that case (because from the usage site, we don't even know about S). Do you agree?

I mentioned the non-final class method situation in another comment. Should it always be an error to declare one of those as @inline(always)?

In general, I think there are up to four situations we need to consider when something is marked @inline(always):

  1. The function can definitely be inlined at the usage site.
  2. The function can never be inlined and the compiler should diagnose an error to try to declare it that way.
  3. The function can sometimes be inlined but not for some specific usage site and the compiler should diagnose an error at the usage site.
  4. The function can sometimes be inlined but not for some specific usage site and the compiler should not diagnose an error at the usage site.

I'm not sure whether there are any cases of (3) that exist, though.

I think the proposal would benefit if we could clearly show examples of each of the situations above. My reading of it currently is that the examples that just say "not guaranteed to be inlined" aren't always clear because the proposal says that in some situations those are diagnosed and in others they aren't. Can we see explicitly the situations when they are and aren't?

Copy link
Contributor Author

@aschwaighofer aschwaighofer Sep 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This carve-out seems like it defeats some of the goals of the guarantee and associated diagnostic, since the proposal says in one place that we should emit a diagnostic when it's not guaranteed, but then it lists these exceptions. I'm trying to think of concrete cases that are left where we can decide that inlining isn't possible and would diagnose it.

I will list the cases as requested. In the initial version of the proposal I required explicitly spelling out @inlinable if @inline(always) was requested on public'ish (public, package, open) declarations, so the set of diagnostics was bigger. I was convinced by feedback on the pitch that we should imply @inlinable instead of being explicit about it.

Yes, the guarantee is mostly upheld by declaration side "measures" (implying @inlinability or explicitly spelling it out for public declarations).

The recursion case, because the cycle can be introduced by another function, is the only instance that can be viewed as sort of a "usage side" diagnostic (3. in the list above).

The protocol/concrete type case is one where it makes sense to not diagnose. For example,

protocol P { func someFunc() }
struct S: P {
 @inline(always) func someFunc() {}
}

In this case, if someFunc is called on an instance of the concrete type, we know we can inline that, but if it's called on a generic or existential constrained to P, we couldn't. That seems fine, and I don't think it makes sense to diagnose in that case (because from the usage site, we don't even know about S). Do you agree?

Yes. That was what I meant to capture in the sentence:

As outlined earlier the attribute does not guarantee inlining or diagnose the
failure to inline when the function value is dynamic at a call site: a function
value is applied, or the function value is obtained via class method lookup or
protocol lookup.

I agree, with both of your comments that I should change the proposal to make non-final class method annotation an error; and that I should add the protocol call with a struct's conformance as an example that we don't diagnose and don't guaranteed.

I mentioned the non-final class method situation in another comment. Should it always be an error to declare one of those as @inline(always)?

Yes. I think that is more in spirit with the intention of @inline(always) being an optimization control. There is no way of directly calling the function (unlike for methods of struct/enum's), semantically it has to go through dispatch so we can't guarantee it. Therefore, I agree with the position we should be flagging this as an error.

I think the proposal would benefit if we could clearly show examples of each of the situations above. My reading of it currently is that the examples that just say "not guaranteed to be inlined" aren't always clear because the proposal says that in some situations those are diagnosed and in others they aren't. Can we see explicitly the situations when they are and aren't?

Yes.

Comment on lines +107 to +109
A sufficiently clever optimizer might be able to derive the dynamic value at the
call site, in such cases the optimizer shall respect the optimization control
and perform inlining.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you elaborate more on cases where this happens in practice, if any?

For example, if we consider the following code:

@inline(always)
func binaryOp<T>(_ left: T, _ right: T, _ op: (T, T) -> T) -> T {
  op(left, right)
}

@inline(always)
func add(_ left: Int, _ right: Int) -> Int { left + right }

_ = binaryOp(5, 10, add)  // (1)
_ = binaryOp(5, 10) { add($0, $1) }  // (2)

Could I expect the Swift optimizer today to fully inline (1), (2), both, or neither?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The paragraph meant to express that there are cases beyond the case we guarantee where the Swift optimizer in say optimizing mode (e.g -O or -Osize) is more aggressive and will (should) take the @inline(always) into account as part of its heuristics.

The cases you mention are optimized today at -O(size) but are not optimized in -Onone and should not be expected to.

I will incorporate this example to make this more clear.

Comment on lines +102 to +105
We only guarantee inlining if the annotated function is directly referenced and
not derived by some function value computation such as method lookup or function
value (closure) formation and diagnose errors if this guarantee cannot be
upheld.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed below, I think this paragraph/section might benefit from a bit of wording, since this seems to imply that we'll definitely diagnose cases where it isn't guaranteed but then later we discuss some situations where it's not diagnosed. I think the word "guarantee" here is a bit confusing since it seems to imply both situations.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. Re-reading this paragraph it is very misleading. I will rework it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
LSG Contains topics under the domain of the Language Steering Group
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants