SE-0376: Function back deployment

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