Compiler Version Directive

@jrose I heavily rewrote the proposal to address @tkremenek's remarks and to improve clarity. Could you give me some feedback on it? By the way, I don't understand why we need to stick with version 4.1.50 if this proposal is accepted. Can you give me an example that the proposal would break?

A bit late in responding, but we still need 4.1.50 so that 4.1 compilers can distinguish between Swift 4.1.x and Swift 4.2-in-4-mode! If you need to be compatible with a 4.0 or 4.1 compiler, you have to keep using #if swift a little longer.

Hello everybody,

I'd like to contribute to this thread with a sample code. It not only exemplifies this thread, but this other one as well: C interoperability, combinations of library and OS versions thread.

It shows how many different techniques have to be used in order to crawl through compiler options:

  • #if CUSTOM_OPTION
  • #if swift(...) || ... (the monster @hartbit wants to replace with #if compiler(...))
  • if #available(...)
  • the really nice @jrose's workaround against code duplication.
  • very long comments because otherwise this piece of code is really really challenging.
static func api(_ db: Database) -> UnsafePointer<fts5_api> {
    // Access to FTS5 is one of the rare SQLite api which was broken in
    // SQLite 3.20.0+, for security reasons:
    //
    // Starting SQLite 3.20.0+, we need to use the new sqlite3_bind_pointer api.
    // The previous way to access FTS5 does not work any longer.
    //
    // So let's see which SQLite version we are linked against:
    
    #if GRDBCUSTOMSQLITE || GRDBCIPHER
        // GRDB is linked against SQLCipher or a custom SQLite build: SQLite 3.20.0 or more.
        return api_v2(db, sqlite3_prepare_v3, sqlite3_bind_pointer)
    #else
        // GRDB is linked against the system SQLite.
        //
        // Do we use SQLite 3.19.3 (iOS 11.4), or SQLite 3.24.0 (iOS 12.0)?
        // We need to check for available(iOS 12.0, OSX 10.14, watchOS 5.0, *).
        //
        // This test requires the Swift 4.2 compiler.
        //
        // It does not need the Swift 4.2 language, though: the Swift 4.2
        // compiler running in Swift 4.0 compatibility mode is OK.
        //
        // On top of that, we want to preserve compatibility with Xcode 9.3+.
        //
        // So let's check exactly which compiler version we are using.
        //
        // Fortunately, this horribly complex check has been solved
        // by @hartbit: see https://forums.swift.org/t/compiler-version-directive/11952
        // and https://github.com/hartbit/swift-evolution/blob/compiler-directive/proposals/XXXX-compiler-version-directive.md
        #if swift(>=4.1.50) || (swift(>=3.4) && !swift(>=4.0))
            if #available(iOS 12.0, OSX 10.14, watchOS 5.0, *) {
                // SQLite 3.24.0 or more
                // setup: Xcode 10.0, SWIFT_VERSION = 4.0, iOS 12
                // setup: Xcode 10.0, SWIFT_VERSION = 4.2, iOS 12
                return api_v2(db, sqlite3_prepare_v3, sqlite3_bind_pointer)
            } else {
                // SQLite 3.19.3 or less
                // setup: Xcode 10.0, SWIFT_VERSION = 4.0, iOS 11
                // setup: Xcode 10.0, SWIFT_VERSION = 4.2, iOS 11
                return api_v1(db)
            }
        #else
            // SQLite 3.19.3 or less
            // setup: Xcode 9.4.1, iOS 11
            return api_v1(db)
        #endif
    #endif
}

private static func api_v1(_ db: Database) -> UnsafePointer<fts5_api> {
    // Code that targets SQLite before 3.20.0
    ...
}

private static func api_v2(
    _ db: Database,
    _ sqlite3_prepare_v3: @convention(c) (OpaquePointer?, UnsafePointer<Int8>?, Int32, UInt32, UnsafeMutablePointer<OpaquePointer?>?, UnsafeMutablePointer<UnsafePointer<Int8>?>?) -> Int32,
    _ sqlite3_bind_pointer: @convention(c) (OpaquePointer?, Int32, UnsafeMutableRawPointer?, UnsafePointer<Int8>?, (@convention(c) (UnsafeMutableRawPointer?) -> Void)?) -> Int32)
    -> UnsafePointer<fts5_api>
{
    // Code that targets SQLite 3.20.0+
    ...
}

full file

OMG I totally missed that SE-0212 was implemented :sweat_smile:.

I can't use it because I need to support Swift 4.1: this is no big deal, I'll wait.

But in an ideal world, I would be able to use && and || between the various checks:

static func api(_ db: Database) -> UnsafePointer<fts5_api> {
    #if GRDBCUSTOMSQLITE || GRDBCIPHER || (compiler(>=4.2) && #available(iOS 12.0, OSX 10.14, watchOS 5.0, *))
        return api_v2(db, sqlite3_prepare_v3, sqlite3_bind_pointer)
    #else
        return api_v1(db)
    #endif
}

private static func api_v1(_ db: Database) -> UnsafePointer<fts5_api> {
    // Code that targets SQLite before 3.20.0
    ...
}

private static func api_v2(
    _ db: Database,
    _ sqlite3_prepare_v3: @convention(c) (OpaquePointer?, UnsafePointer<Int8>?, Int32, UInt32, UnsafeMutablePointer<OpaquePointer?>?, UnsafeMutablePointer<UnsafePointer<Int8>?>?) -> Int32,
    _ sqlite3_bind_pointer: @convention(c) (OpaquePointer?, Int32, UnsafeMutableRawPointer?, UnsafePointer<Int8>?, (@convention(c) (UnsafeMutableRawPointer?) -> Void)?) -> Int32)
    -> UnsafePointer<fts5_api>
{
    // Code that targets SQLite 3.20.0+
    ...
}

:heart:

The reason that compiler(…) and swift(…) are special is because there's no guarantee that the code within the #if even parses if the requirement isn't met, because we might have added some new syntax to the language. (Like, say, throwing subscripts?) The other conditions still require that the disabled code be syntactically valid, for syntax-highlighting and potential structural editing purposes.

It would probably be safe to add a rule that says "you can combine compiler(…) and swift(…) with other conditions, but then the other side is always parsed", but that makes the "special" behavior of those two a little more subtle.

1 Like

Thanks Jordan, I (mostly) see why it is difficult. It looks like #if compiler and #if swift are not at all handled at the same step as #if FOO and if #available during the compilation process, and their reunification is a really hard and subtle work.

I also understand that parsability of code is a hard requirement (I think SourceKit, eventual future hygienic macros, and tools I'm not even aware of).

I wish that a solution would have been to process the AST beforehand so that...

#if FOO && swift(>=4.2)
...
#endif

...would be interpreted as:

#if FOO
#if swift(>=4.2)
...
#endif
#endif

But the AST can't be built without knowing the Swift compiler version :sweat_smile:. Finding the first #endif indeed depends on the Swift version (future raw strings are expected to change this game, for example).

I guess I've just rewritten your thoughts, with less precision. Anyway, thank you very much for your answer: it's good to have an ear in such a situation. I expect that as SPM strengthens, more library developers will come with similar needs.