Introduce `#if stdlib` or clarify `#if swift`

Yes, the #if canImport(Swift, _version: 5.7) syntax should work today as a solution to the problem. It would be relatively low effort to turn it into an official feature with operator syntax replacing the _version: argument and I think there have been good arguments made in this thread for doing so:

  • It solves a real problem that comes up repeatedly.
  • The design is flexible; it's not overly specific to either the standard library or Apple's SDK distributions.

The main problem I see with the design is that it is not very straightforward to discover the version number you ought to use in order to detect the condition you're interested in. The standard library follows a versioning convention that makes this easier but the versioning of other modules can be somewhat arbitrary. You may need to inspect the .swiftinterface distributed with the library to check the version you have currently or ask the library owner to tell you which version to check in order to solve your problem. I personally think that the utility of the feature outweighs the awkwardness, though, and I'd rather we made this simple extension of canImport available sooner rather than wait for a more ambitious, more ergonomic design to be worked out (which wouldn't be precluded by exposing this functionality now).

6 Likes

I'm happy to get the functionality in even though the canImport name doesn't make a lot of sense for this particular use case. But as long as it functions the way we want it'll be fine.

1 Like

Well, technically you can import Swift to import the stdlib but it's always implicitly imported by default so nobody does it. So canImport(Swift) is actually the most consistent naming when you think about it.

In fact, this use case seems like something where we need the explicit "can import version X of library Y" (as opposed to basing it on something like "can import specific type") because the issue posed by the introduction of primary associated types was independent of any given symbol being present in the interface or not—it was a meaningful change to the capabilities of existing symbols.

1 Like

I don't really agree with this or @davdroman's point. #if canImport and #if module (or whatever name) are two different questions specific to two different scenarios. That you could used the versioned form of canImport to answer a question about module versions doesn't mean you should. This would be rather strange to see:

#if canImport(Swift, version: >=5.7)
func someFunc() {}
#endif

Where's the import I just asked about?

It's even weirder if we have an explicit import:

import Foundation

#if canImport(Foundation, version: >=100)
func someFunc() {}
#endif

So I imported the module, then asked if I can import a particular version, even while it's already imported?

So while canImport could answer the question we're asking, it doesn't seem to be the right form in which to ask the question in the first place.

I find the reuse of canImport compelling because (IMO) it appropriately emphasizes when the check is appropriate. As others have mentioned, it’s important that whatever form this feature takes is not easily misunderstood as a runtime check when that’s what the author really wants. IMO, canImport passes this test, but something less specific like module does not.

Well, this is partially weird because nothing you’ve written inside the #if depends on any library. But in general it seems reasonable that the specific name you’re testing with canImport may not appear inside the block, regardless of whether you’re testing for Swift or a different module name.

That seems… entirely legitimate to me? You’re unconditionally importing whatever version of the library you’re compiling against, but then compiling some code only when you’re importing a specific version (or greater) of that library.

2 Likes

How so? canImport naturally answers the question "Can I import this?", so how is it the appropriate question to ask when I want to see if the version of a module I already have meets a particular predicate?

Until we gain the ability to import a module with a particular version, it doesn't seem an appropriate question to ask unless we're importing something. In fact, I would support adding an error or warning if you checked canImport but then didn't import the thing you checked.

Of course, this is just about proper naming. I'd be glad to get the functionality.

Right, this seems to express exactly the thing we want to express.

I also agree with you it's nice that the spelling emphasizes useful parallels to use cases of unconditional canImport. Indeed, one might imagine that if we had this feature first but named it something else (for the sake of argument, flugeltorp), the unconditional canImport might have been spelled flugeltorp(Swift, version: *).

3 Likes

With the version specifier, it answers the question "can I import version X of library Y?", which is precisely the question you want to answer for, e.g., using primary associated types absent some sort of bespoke #if stdlibHasPrimaryAssociatedTypes check.

I'm definitely not opposed to other names, but canImport feels pretty good to me.

I think there's reasonable things to warn about, particularly for the non-version-gated canImport, but I definitely don't expect it to be the case that every canImport(X) block contain import X somewhere within. A setup like the following seems perfectly legitimate:

// A.swift

#if canImport(X)
import X
#endif

...

#if canImport(X)
func someAPIThatUsesX() { ... }
#endif

// B.swift

#if canImport(X)
func someOtherAPIThatUsesXTransitively() {
  someAPIThatUsesX()
}
#endif
1 Like

Likewise, not every #if __has_include(<X.h>) has an #include within it.

2 Likes

That naming makes a bit more sense for both uses. If Swift had hasModule, the more general nature lends itself to answering many questions. My only issue with canImport is that it appears to answer only one.

While I'm of course delighted that the functionality exists in underscored form, I'd echo @Jon_Shier's concern about canImport not necessarily being an obvious spelling of this - at least for the standard library.

We would have:

  • #if swift(>=X), and
  • #if canImport(Swift, version: >=X)

And they would mean entirely different things. How do we teach people which one is appropriate in a particular circumstance? How do we expect them to remember it?

Which gets back to a point I was making earlier - what exactly is a version of Swift? Is it the language mode? Is it the standard library? Both call themselves "Swift", but their versions may not match.

Perhaps it would be worth allowing Stdlib as an alias for Swift, and renaming #if swift to #if languageVersion, so the result would be something like:

  • #if languageVersion(>=X)
  • #if compiler(>=X)
  • #if canImport(Stdlib, version: >=X)

So each condition would be clearer about what it actually checks.

2 Likes

Yeah, IMO, #if swift(...) is ambiguous enough that it might be worth renaming to something like languageVersion (which I think is probably the most accurate name) quite apart from encouraging users to write canImport(Swift, ...) more. It sure looks like #if swift(5.7) should do the same thing that #if canImport(Swift, >=5.7) is proposed to do.

Once (if) we've renamed swift(...) to languageVersion(...) I feel less strongly about having a Stdlib alias for Swift.

This would be very unfun to use. You would have to check compiler first before you can check a new compile-time conditional such as languageVersion, and then you would need to check something else (depending on how implemented) before you would know if an alias is recognized for Swift. In practice this incantation would have to be copy-pasted from an authoritative source (because who would have the time to figure it out from scratch), and it will be useful at most once a year and possibly obsolete if Apple changes its software distribution practices before it's even used a handful of seasons.

Rather, a non-vendor–specific change we could do which would require no new syntax is to have #if swift evaluate min(languageVersion, stdlibVersion). That the standard library doesn't have its own name or versioning reflects its tight coupling with all the other components it's versioned with; one of the decision criteria about whether something belongs in the standard library is whether it is required to support a language feature.

I think arguably the right thing to do here is not to foist the scenario where language version outstrips the standard library version onto users in the form of additional knobs to account for. By construction that is a configuration where new language features can lack the requisite non-optional library support that must come from the standard library. Thus, where a user is already asking to conditionalize on language features (rather than the underlying compiler), we can synthesize this observation by providing an all-in-one #if swift.

Firstly - if both languageVersion and the alias come in the same compiler version, one compiler version check could cover both of them.

Secondly - whether you even need that additional check depends on your minimum supported compiler version. If we added this in Swift 6, only projects which still support Swift 5 would need that check at all. In the fullness of time, we'd transition to a better overall model.

This applies to all compile-time checks - for example, #if hasFeature(...). They all need to be accompanied by a compiler version check if the code is intended to be compatible with older compilers.


Already responded to this:


This is essentially the other option that I suggested:

I'm not opposed to that - again, the most important thing is giving developers certainty - but combining these version identifiers is also a delicate business.

It would preclude code which wishes to use new language features that do not depend on the standard library, and would make life even more difficult if a toolchain did have reason to ship with an older standard library (to the extent that we may as well prohibit that completely, and declare such configurations to be "invalid").

But if we're going to continue to allow mixing and matching versions and say they are allowable toolchain configurations, it seems wise to allow testing each part.

In general, aliases ship with the SDK—that is, unless it's hardcoded into the compiler (hence the parenthetical I used about "depending on how implemented"). Even if it were hardcoded, it would only ship in the same version of the compiler as #if languageVersion if the two features were implemented together, which as @Jumhyn mentioned doesn't have to be the case.

If I understand you, though, you're explicitly proposing that both of these conditions should hold in any solution—fine.

If you restrict the solution only to Swift 6 (by which I assume you mean the compiler version and not the language mode, but notably no announcement has been made that the next yearly release will coincide with a major version increment), then you are limiting the solution to a speculative future where it is a toss-up whether the problem will even exist. And the fullness of time in which you wouldn't need multiple compiler conditionals to use your solution may arrive many years later than the fullness of time in which no vendor ships mismatched language versions and standard libraries.

And those are equally unfun—far from refuting my point, it's in fact experience with hasFeature that informs why I'm saying that a separate languageVersion check would be unfun.


We cannot "require" any vendor to ship SDKs in any particular way or other; we can however, change what #if swift means: those are quite different suggestions in my view.

No version identifiers are being "combined"; the whole point is that they were never separated to begin with, not in a thought-through user-manipulable way.

Again it's not up to us to declare how a vendor should ship Swift.

I am explicitly arguing, though, that it's desirable to preclude code from checking for the ability to use language features from versions ahead of the available standard library. The distinction between "language features that depend (at compile time) on the standard library" and "language features that don't depend (at compile time) on the standard library" is not meant to be user-facing (somewhat analogous to how we say that clients of libraries shouldn't have to care if a property is stored or computed). Synthesized Equatable conformance, for example, is currently hardcoded but may one day be moved into the standard library, and in the ideal case this would be completely seamless for users; if it doesn't otherwise change observable behavior, my understanding is that it wouldn't even be considered a language-level change that requires Evolution review.

This is a fuzzy sentence as to who "we" are and what it means to "allow." The language workgroup hasn't considered in the past how language features behave in the context of a compiler that's older than its accompanying standard library. If that is to be the case going forward, I'd expect the vehicle for that sort of policy change wouldn't be in the form of a proposal.

I'm happy to pitch de-underscoring versioned canImport to support this use case and also the other cases in the threads I linked to where the standard library version is not involved. Before I do, though, I want to get a sense from the folks in this conversation about how important it is to you to also address these concerns:

I think it's true that in the particular case of testing the version of the standard library that the distinction with language version is subtle and probably inscrutable to the average Swift developer. Short of carving out a special case for the standard library in the proposal, though, being able to write #if canImport(Swift, version: >=5.7) is going to naturally be possible, given that the standard library's module name is Swift. The proposal could include aliasing the name of the standard library and diagnosing the potentially confusing reference to bare Swift if we felt it was justified enough, but I also think it's a concern that's separable from unlocking the capability in general.

1 Like

Agree—I think unlocking the general capability would be orthogonal.

The specific issues to do with the standard library and Apple toolchains are, I think, separable and (as I argue above) there are alternatives to consider there distinct from the overall capability discussed here.

1 Like

Yeah, I think this is the one aspect of the issue that is squarely within the bounds of the Swift project's scope of review. Canonicalizing canImport(_:version:) will give us two compiler conditionals that look very similar but are in fact subtly different.

I think it's worth some discussion of this issue in such a proposal, but I don't personally consider it particularly blocking. I think #if canImport(Swift, version: >=5.7) is quite clear about what it will do, and so without any other mitigation we would perhaps be making #if swift(>=5.7) marginally more confusing. And OTOH, having a way to actually spell the thing that some folks might think #if swift(>=5.7) does may actually reduce confusion?

2 Likes

Having read through this, it’s clear to me the difference between compiler(>=x) and canImport(Swift, version: x). My question is, what does swift(>=x) do that the other two can’t?