Compiler Version Directive

What do people think of the following proposal? An implementation is already available:

@jrose you’ll be interested by this

5 Likes

This sounds great to me! I would leave out the "deprecate _compiler_version" part—that’s more a hack for testing between releases within Apple, and it’s already underscored—but other than that this makes sense. (You might have seen we ran into this hard when we decided to do a 4.2 that was going to have some incompatible changes.)

I do have one design question: does the current version number continue increasing for #if swift? That is, for compiler version 5.1, will swift(>=5.1) be true or false?

(I’m not sure which way I lean on this.)

So this doesn’t actually have anything to (visibly) do with _compiler_version, it just uses it internally to make the #swift feature treat versions of Swift supported by particular versions of compilers the same?

As an alternative (or in addition, since I like the change), perhaps we can make the # directives more flexible with version checking? i.e. Instead of just >= or =, support a more full range of comparison operators?

We deliberately don’t do anything other than >= and ! because of patch releases, but since < is just the combination of those two it’d be okay. @hashemi even worked on an implementation in SR-6852. No need to roll it into this proposal, though.

Note that David’s not proposing to remove #if swift(…). That’s still (mildly) useful for a file you can drop into a larger project.

I’m fine with that.

If we’re not running in any compatibility mode, I’d say yes. Are you asking this because getEffectiveLanguageVersion prohibits minor version numbers where compatibility is not broken?

*shrug* Something like that. “If you need to test for language features, maybe you should use compiler(>=) instead of swift(>=) in general.” I’m not actually sure which is better, but the proposal should pick one (and possibly mention the other as an alternative).

(We also probably eventually want to do something like Clang’s __has_feature(xyz) instead, but we’re not there yet.)

I’ve updated the proposal to not mention _compiler_version.

Can you elaborate what you are asking? Perhaps I completely misunderstood. I don’t see any scenario where it would make sense to stop increasing version numbers for swift. Are you talking about completely stopping bumping it, or just stop bumping minor versions?

I meant “just stop bumping minor versions”, but I’m realizing that even that probably doesn’t make sense given that the version numbers only weakly mean anything anyway and are chosen by Apple, not the community. Still, there’s a question of “if I’m using the latest version, is it better to use #if swift or #if compiler?”. As @tkremenek pointed out on the proposal PR, those two are connected now, but wouldn’t have to be in the future.

(I don’t have a good answer to this question at the moment, but it does seem important.)

I’m sorry but I’m getting very confused. The current versions of Swift are:

Swift 3.1: Latest version of Swift 3 under the Swift 3 compiler.
Swift 3.2: Swift 4 compiler under Swift 3 compatibility mode.
Swift 3.3: Swift 4.1 compiler under Swift 3 compatibility mode.
Swift 4.0: Swift 4 compiler
Swift 4.1: Swift 4.1 compiler

Confusion 1: I’m not sure anymore why versions 3.2 and 3.3 were created. Can somebody explain? Why wasn’t a solution like the one in this proposal used to start with?

Some libraries want to support being compiled under mulitiple versions of the Swift compiler. This sometimes requires conditionally compiling portions of the code depending on the version of Swift with the swift directive. This can become quite messy when a library stretches multiple major versions of the compiler and minor versions of Swift (like 4.1) introduce changes which are applied whatever the compatibility version (as is often the case with Standard Library changes). It requires messy conditionals:

#if swift(>=4.1) || (swift(>=3.3) && !swift(>=4.0))
	array.compactMap({ $0 })
#else
	array.flatMap({ $0 })
#endif

If we accept and merge this proposal, we can simplify those conditionals for future versions of Swift:

#if compiler(>=4.2)
	// Use feature introduced in 4.2
#else
	// Use backwards compatible implementation
#endif

Of course, for applications or libraries which always require the latest version of the compiler, the swift and compiler directive would be semantically equivalent.

Confusion 2: If this proposal is accepted, would the swift directive still be useful?

It seems the answer to this question is yes only if certain features introduced in newer versions of the compiler are not backported in compatibility mode:

#if swift(>=4.2)
// Use feature only enabled in Swift 4.2 (no compatibility mode)
#endif

Table

Here’s a table to recap and help me think:

F1: Feature introduced in 4.2 and available in compatibility mode
F2: Fetaure introduced in 4.2 and not available in compatibility mode
F3: Feature introduced in 5.1 and available in compatibility mode
F4: Fetaure introduced in 5.1 and not available in compatibility mode

Compiler Invocations swift directive version compiler directive version F1 F2 F3 F4
Swift 3.1 3.1 N/A
Swift 4.0 4.0 N/A
Swift 4.0 (–swift-version 3) 3.2 N/A
Swift 4.1 4.1 N/A
Swift 4.1 (–swift-version 3) 3.3 N/A
Swift 4.2 4.2 4.2 X X
Swift 4.2 (–swift-version 3) 3.4 4.2 X
Swift 4.2 (–swift-version 4) 4.1.50 4.2 X
Swift 4.2 (–swift-version 4.2) 4.2 4.2 X
Swift 5.0 5.0 5.0 X X
Swift 5.0 (–swift-version 3) 3.5? 5.0 X
Swift 5.0 (–swift-version 4) 4.3? 5.0 X X
Swift 5.0 (–swift-version 4.2) ? 5.0 X X
Swift 5.1 5.1 5.1 X X X X
Swift 5.1 (–swift-version 3) 3.6? 5.1 X X X
Swift 5.1 (–swift-version 4) 4.4? 5.1 X X X
Swift 5.1 (–swift-version 4.2) ? 5.1 X X X
Swift 5.2 5.2 5.2 X X X X
Swift 5.2 (–swift-version 3) 3.7? 5.2 X X X
Swift 5.2 (–swift-version 4) 4.5? 5.2 X X X
Swift 5.2 (–swift-version 4.2) ? 5.2 X X X

Using feature F1 with swift directive alone

#if swift(>=4.2) || (swift(>=3.4) && !swift(>=4.0))
// use F1
#endif

Using feature F1 with compiler directive alone

#if compiler(>=4.2)
// use F1
#endif

Using feature F2 with swift directive alone

#if swift(>=4.2)
// use F2
#endif

Using feature F2 with compiler directive alone

Impossible.

Using feature F3 with swift directive alone

This is a clear example of why its not tenable with swift alone:

#if swift(>=5.1) || (swift(>=4.4) && !swift(>=5.0)) || (swift(>=3.6) && !swift(>=4.0))
// use F1
#endif

Using feature F3 with compiler directive alone

#if compiler(>=5.1)
// use F1
#endif

Using feature F4 with swift directive alone

#if swift(>=5.1)
// use F2
#endif

Using feature F4 with compiler directive alone

Impossible.

I think the key here is to not check the compiler version but the Standard Library version. This could be achieved with more generic module version conditions which would be dynamically checked at runtime:

if #moduleVersion(Cocoa, >=6.11) {

}

if #moduleVersion(Swift, >=4.1) {

}

Take in account that once ABI stability is achieved the Standard Library version could be not related in any way with the compiler version.

1 Like

Run-time checks are different from compile-time checks. This proposal is concerned with compile-time checks, i.e. it really is about what language features are available to you when you compile.

But you shouldn’t relate the Standard Library version with the compiler version. The proposed problem should be resolved dynamically at runtime not statically at compile-time. Moreover, once macOS includes the Swift Standard Library this directive couldn’t be used.

I did mention the Standard Library in the proposal (probably should not have), but this proposal is really about the compiler version. A better example might be: how might I conditionally remove my hashValue implementation when the compiler synthesizes its own?

  • !swift(>=4.1) is too restrictive because I’m missing out on that compiler implementation when running in Swift 3 compatibility mode.
  • !swift(>=3.3) || (swift(>=4) && !swift(>=4.1)) is correct but complex and error prone.
  • !compiler(>=4.1) is much better.

@jrose do you have any answers that might resolve my confusions from my previous post?

@hartbit, I’m also a bit confused about this. I’ve read the draft proposal; the previous topic; and the blog post.

Your table of Compiler Invocations is incomplete, because there will be a -swift-version 4.2 (Basic/Version.h, Basic/Version.cpp). And the compiler directive version for Swift 3.1 … 4.1 should be empty.

Your example of conditionally using compactMap or flatMap might be unnecessary, because the old APIs are only deprecated since version 4.1 (SR-6970). And as @pvieito points out, if the standard library eventually becomes a system library (or framework), then would this need to be a run-time condition?

Instead of introducing a new #if compiler(...) directive, could this proposal update the existing #if swift(...) directive, by adding argument labels?

I’ve updated to table with this information. Let me know if there’s anything else I should fix.

I should not have used that example. There are valid features that are not part of the Standard Library, like Hashable or Equatable synthesis, etc…

Are just proposing a different naming?

#if swift predated Swift 4, so originally there was no distinction between “compiler/language version” and “compatibility version”. When we got to Swift 4, we had to decide what #if swift(>=4) would mean.

  • Code that was supposed to work with the new Swift 4 and the old Swift 3.1 should have a way to test the difference between the two (easy).

  • Code that was supposed to work with the new “Swift 3 compatibility mode” and the old Swift 3.1 should have a way to test the difference between the two (medium).

  • (?) Code should be able to work with both the new Swift 4 and the new “Swift 3 compatibility mode”

That last bullet is definitely the weakest, and maybe we should have made the behavior of #if swift always advertise the compiler as Swift 4, instead of sometimes saying “3.2”. But there was actually one additional subtle point:

  • Objective-C code should be able to tell if it’s working in Swift 3 mode vs. Swift 4 mode, to maintain compatibility.

I don’t think we’ve needed to use it for anything, but you can actually check if you’re running a new enough version of Swift from Objective-C code. And that’s definitely using the “effective” language version, the compatibility version, because the main point of the whole effort was to make sure Apple’s SDKs looked as close as possible to they way they did in Swift 3.1.

So, with that in mind, we could have either made #if swift mean something different than the Objective-C check, or we could have added something new like #if compiler, or we could have pretended there was also a Swift version called “3.2”. We went with the latter; it was expedient.

(We (or at least “I”) never really expected -swift-version 3 to survive all the way to a compiler that supported -swift-version 5, so the increment-versions-in-parallel thing didn’t seem quite as egregious at first.)

But 4.2 has become an issue, where we need a new version for non-backwards-compatible stdlib and SDK changes, but no longer have room to increment -swift-version 4. That’s the weird “4.1.50” in David’s table, and why we need a solution soon. (Unfortunately, nothing can save the “4.1.50”, because any solution has to also work with the existing Swift 4.1 compiler and perhaps 4.0 as well.)


#if swift has only ever been useful if you expect to be compiling (1) with multiple compilers, or (2) with multiple -swift-versions. This proposal offers another way to do (1), and (2) is a lot rarer than (1), but it does happen, with the most common use case being “drop this file into your project; it should Just Work no matter what your configuration”. We also can’t remove #if swift because, well, obviously it’s doing something in tons of people’s existing code.

As you posted earlier, “the version numbers only weakly mean anything anyway and are chosen by Apple, not the community.” If the -swift-version 4.2 mode is going to cause problems, why not go back to the old rule of only allowing major version numbers with that compiler flag? Either delay the non-backwards-compatible changes until Swift 5, or release two major versions this year (“4.2” becomes “5.0”, and “5.0” becomes “6.0”). Then there’s room to increment the compatibility-mode version numbers, and the existing solution continues to work.

Thanks for the historical background. It makes more sense now. The only question I’ve got left: do you think that the proposal is the correct solution? If yes, I think we should stop bumping old Swift versions, but swift(>=5.1) should continue to be true in 5.1 because that’s what people will expect.

When I say “version numbers are chosen by Apple”, that includes non-engineering factors in the decisions. I can’t really go into further details.

Yes, I do think this proposal is a good answer, and I think bumping the new version but not older ones is reasonable. I wish we had a better rule about "when to use #if compiler and when to use #if swift", but it might just be “prefer #if compiler unless you’re trying to distinguish Swift versions within the same compiler” in the long run. (Obviously #if swift is still usable for compatibility with 4.0 and 4.1 in the short term.)