SE-0376: Function back deployment

+1 This looks good to me.

I think that's not necessarily true, though I doubt the following use-case would be common: Imagine a company sells a product that allows plug-ins (maybe it's an app controlling a hardware gadget). For third-party plug-in devs they provide an SDK but naturally they don't want to publish their entire source code. Yet, some features only may be available starting with specific newer versions of the app, for that the SDK could @backDeploy at least parts those. In other words: The installed app is/provides the dylib that the SDK accesses but the SDK can also contain @backDeployed methods if the app version is behind the SDK version.

Admittedly, it'd probably be easier to just ensure the app receives an update along with each new SDK version, but perhaps that's not always possible (business users might not renew a license, maybe the connected hardware gadget doesn't support it and you don't want to go through the additional work to still release updates for those, or something else).
I think in such cases it would be useful, no?

Of course the Apple platforms (atm) would benefit the most (or rather we would if that's done and Apple uses it frequently!).

I read the entire thing and it gets a +1 from me. I am also indifferent about the name (I think it's reasonably short and gets the meaning across).

A good use case, but @backDeploy is tied to availability and there's no way (that I'm aware of) to have the app SDK provide an availability target such that you can write if #available(TheApp 2.0) or @backDeploy(before: TheApp 2.0). Meaning you can only back deploy relative to the OS version, which has no relation to the app SDK version. Or should the app SDK include its own compiler toolchain?

Defining library-relative availability independent of the OS would be a good feature to have. Until then this attribute is only useful for frameworks that ship with the OS. This is what I meant.

5 Likes

Oh, of course! I hadn't considered that, my bad!

I was focusing so much about the back deploy logic itself that I forgot @availability relates to the OS version (which is tied to the toolchain, I assume fro your comment?).

I still think this is worthwhile, so my +1 stays, but I guess for the scenario I described we would need more. I guess this could build on top of this proposals implementation then, though.

P.S.: This is why I love it here! I've learned so much about language design and what all goes into it, it's really an invaluable experience. :smiley:

2 Likes

Hopefully we can have that in the future - I opened Availability annotations for third party libraries when using Library evolution/ resilience · Issue #60458 · apple/swift · GitHub for it a while ago. I could see us using @backDeploy in conjunction with this for our frameworks, most definitely.

3 Likes

This restriction is not strictly necessary but I do think it can help with clarity of intent. We have found over time that allowing API owners to omit availability entirely from public declarations in library modules is a nuisance because it's too easy to forget to add availability to new APIs when it is necessary to do so for correctness. We have other tooling approaches that can address that, though.

That's right, although it's not that useful to add @backDeploy to functions in non-SDK libraries today, it could become useful in more contexts in the future if the scope of Swift's availability model expands.

1 Like

This is great! I think it's been mentioned at the start, but being able to provide back deployments within the app itself would be a huge benefit. Some of the examples in this post would be much simpler without having to do the Backport stuff and/or availability checks (which for modifiers in SwiftUI is a pain). Maybe it could be spelt like: @available(iOS 13.0, iOS 15.0) instead of the backDeploy attribute for that case.

The author has updated this proposal to adopt the suggestion that @backDeploy and @inlinable not be mutually exclusive. Review remains active and still runs through Monday, November 7.

6 Likes

Sorry for this late review (I've been away from Swift Evolution for some time)! Hopefully we're still using sort of a soft deadline.

In general, I'm in favor of this proposal.

The only thing that I don't like in this proposal is the requirement of having @available along side @backDeploy. As far as I understand, everything that @available expresses should already be inferable from both @backDeploy's parameters and the availabilities of APIs used in the back-deployed function body. It would be great if the reasons for this requirement could be elaborated on in the proposal.

Some bikeshedding: "backDeployed" and "backDeployable" sound better to me than "backDeploy". "backDeploy" is a verb, a command, whereas "backDeployed" and "backDeployable" are adjective, which feel more suitable as attributes (because adjectives are attributes of nouns). Additionally using an adjective allows the new attribute to be more consistent with other attributes. The majority of Swift's existing non-underscored attributes are adjectives, with the minority being nouns, and only 2 being verbs (@dynamicMemberLookup and warn_unqualified_access).

Explicit availability is still useful in case the implementation of the backwards-deployed function changes; you wouldn’t want to implicitly change the minimum supported deployment target by accident. We also wouldn’t want to have to type-check a function body in order to determine its availability for uses elsewhere in the same module.

3 Likes

If this feature is only useful for Apple's SDK engineers, for products distributed in binary form, what is the value in making this a formal part of the language?

I'm not saying there's no value in this feature - clearly there is; but there is an opportunity cost to everything, and if we take up @backDeploy now for this feature in its current shape, it limits what can be done as future needs emerge. And I struggle to see a benefit that makes that worthwhile.

(This is especially true now that we have feature detection and conditional compilation of attributes - even projects like swift-system which have binary and source distributions could support this attribute for their SDK builds without source clients needing to understand the attribute at all)

1 Like

Thanks for pointing this out! This makes a lot of sense to me. Now I see a reason for explicit availability.

I'm guessing the reason for not wanting to type-check function body for availability is for performance. Although, what's the significance for uses in the same module?

Ah, it’s specific to the same module because once you’ve compiled a module, the compiler could write the inferred availability in the swiftmodule/swiftinterface, and thus not need to repeat the work. But during (debug-mode) compilation, the various files in the module get compiled in parallel by default, and there’s no reason to even parse function bodies in other files, because all important information is outside the body.

Inferring availability for declarations that are internal to a module is an interesting idea, but for public declarations availability is an attribute that must not change implicitly because that could have unintended effects on source compatibility (I'm repeating what Jordan said here but I just want to emphasize it). We could allow @available to be omitted from declarations that have @backDeploy, but what that would mean to the compiler is that the declaration is implicitly available at the minimum possible deployment target (e.g. iOS 8), just like it already does today for normal declarations. For existing platforms supported by @available it's unlikely that many declarations that would adopt @backDeploy could even take advantage of omission, but it's possible that it could be convenient on younger platforms. I am biased towards an explicit model because the fact that @available can be omitted is a frequent footgun for SDK engineers but I do acknowledge that this is not a strictly necessary restriction.

Isn't this self-checking at compile time? The back-deployed function cannot depend on things buried very deep into the framework's code because it has to be emitted into older clients. The compiler should be pretty reliable about checking for an availability mistake if the back-deployed function only references public APIs.

I think it's more likely you'll forget or make a mistake with the @backDeploy attribute because it's the one determining the ABI cutoff point in this case. Forcing @available to be there when you @backDeploy does not help as the compiler will be able to tell you if it's needed.

Actually, perhaps an interesting technique for back deployment would be to label all new APIs as @backDeploy(before: someOS <newversion>) and then let the compiler guide you to decide what to put in @available based on the availability of the other public symbols it references. At worse, the suggested @available will have the same cutoff version as @backDeploy, making the later redundant.

You're right that these mistakes can and should be caught at compile time, but even assuming they are (*see long-winded aside below for why that's actually complicated) I still think it's valuable for authors to think about and explicitly state their intent since this remains a source compatibility concern. For example, maybe it is possible for the current implementation of your back deployed function to be compiled with the minimum deployment target. But do you want to be locked into supporting that deployment target? Outside of the standard library I think it's rare that omitting availability for public declarations in a library evolution enabled module is the right thing to do. All that said, I'd be fine with removing the restriction; I just think it's valuable and has very little cost.

*More detail on why this topic is complicated, for those who are interested.

The compiler was actually not capable of catching these mistakes in the configurations that SDK developers build modules in until very recently, and even now you have to use a special flag to opt-in. The reason is that SDK modules are built specifying the current deployment target (after all, the resilient code compiled into the module is only going to run on the OS it ships with). By default, the availability checker uses the deployment target as a floor for everything in your program and ignores OS versions earlier than the deployment target. This means that SDK libraries effectively weren't being availability checked at all since they always deploy to latest OS and there are therefore no declarations in existence that could be considered potentially unavailable. This behavior is reasonable for apps (and libraries built from source) since inconsistencies for OSes earlier than the deployment target are irrelevant. But library evolution enabled modules are different because they vend an external interface that can be used to compile other modules that might run on any deployment target.

This has been a long-standing deficiency of the availability checker that has lead to many mistakes, especially in inlinable function bodies like ones with @backDeploy. As of Swift 5.7, though, we have a way to let library authors opt-in to letting the compiler distinguish between the parts of the program that will only run on the deployment target vs. the parts of the program that are in the "interface" and need to be checked against a minimal deployment target instead of the deployment target to find potential inconsistencies. We should consider turning it on by default for all library evolution enabled modules but it will need to wait until Swift 6 as that would be source breaking. I can start another thread to discuss this separately.

Even with improved availability checking to catch obvious inconsistencies, mistakes still occasionally happen during development because incorrect availability is not always detectable. For instance, if you introduce a new public struct that only uses standard library types in its external interface, there's no way for the availability checker to know that something is obviously wrong with that declaration.

3 Likes

While I’m sympathetic to the need, this strikes me as something that should be handled in a linter rather than the language specification. There are many things that you can do in the language but probably shouldn’t, which nevertheless produce no diagnostic or at most a warning. Enforcing good practices through compiler errors is not consistent with Swift norms.

Yes in general re linting for stylistic issues and many coding practices, but strong support built into the compiler for helping library authors express their API and ABI intentions is very in line with Swift's direction. If anything we're sorely in need of more built-in features to help make sure that when libraries ship, their APIs are available exactly where intended and their ABI stability guarantees are checked automatically to the maximum extent possible for tooling.

2 Likes

Taking off my review manager hat, would like to also add that there’s a fairly large distance between “users can write code that is inadvisable/problematic” (mostly a non-issue in my mind) and “the path of least resistance will cause users to inadvertently write inadvisable/problematic code.” The boundaries here can be somewhat blurry but IMO the second issue falls squarely within the realm of language design rather than linting.

10 Likes

That is really well put. I'm going to bookmark this phrase for later, as I think it's an imminently useful criterion.

2 Likes