Macro Adoption Concerns around SwiftSyntax

I don't see how language modes are relevant to this.

Swift has never been easy to run older versions since the language evolves too quickly. It is a little better now than the early days, but still not nearly as stable as most other languages. Especially targeting Apple platforms, you can't really run older versions of Swift.

SOP is basically always run the latest version of Swift, but use language modes to compile older source. My assumption is, they will make sure not to break swift-syntax ABI moving forward from when 5.9 is released and they probably won't consider running older releases of the Swift compiler at all.

If the official language guidance is that libraries should only target the latest Swift version, it'd be nice to have that codified somewhere. As you say, it's the norm in other languages for libraries to work across many language versions.

Meanwhile, I think library maintainers think they can and should support many versions of Swift at once. The Swift Package Index shows package compatibility across the most recent 4 versions of Swift and allows package owners to badge their projects accordingly:

image

If, however, libraries should only care about supporting the latest Swift version, and can only support the latest Swift version when including macros, then the language should probably make that expectation clear. That way, resources like SPI would probably be encouraged to simply show the Swift Tools Version instead.

10 Likes

I've written a few macros already and I wonder if it makes sense to do a really light library interface for macros and opt in to the rest of swift-syntax only if you need it.

You can go along way just returning source code in text format without walking the AST at all (or very, very minimally). There might also be room for wrappers over swift-syntax that do the work of maintaining compatibility.

3 Likes

I've always assumed that referred to the Swift language mode or SPM features it supports. For instance if a package says 5.5 I assume it supports async, but I would still compile with 5.8. If the package said 4.0 then I'd compile with 5.8 using the Swift 4 language mode.

This is a great question though since I don't think I've seen it documented anywhere. That just the impression I get. It would be nice to have official guidance if implementing Macros this way.

It refers to whether or not the SPI server farm was able to build the package using a particular version of Swift. If a library is restricted by Swift version, an up-to-date library could only ever have a badge with a single version, like:

image

1 Like

Another comes to mind, given this guidance:

Should a client depend on another macro, which hasn’t released a new version that depends on swift-syntax 510 yet, then SwiftPM will continue to select version 1.2 for the macro. In order for the client to update to version 1.3 of the macro, all macros need to release a version that is compatible with swift-syntax 510. The macro can continue to deliver updates for those clients by creating patch releases such as 1.2.1.

This advice seems to be very precarious for general packages as more and more introduce macros. Back to the URL routing package example: say it depends on a URL parsing package that has macros, and that URL parsing package depends on a parsing package with its own macros, and let's say the tree of dependencies contains an assortment of other packages and dependencies, many of which ship macros of their own. Because all of these packages depend on specific versions of swift-syntax, supporting a new version of Swift makes dependency trees a whole lot more fragile, and much more difficult to update (and if these packages also try to support multiple versions of Swift at the same time, that seems to be impossible at this point). While this problem can apply to dependencies more generally, it goes from an occasional, workable issue to an impossible one pretty quickly.

7 Likes

Interesting on SPI. I don't have experience with it. Does anyone actually build with older versions of Swift or is this just to give the package maintainer peace of mind if their code crashed the Swift compiler between two different releases? I think unit testing on different OS versions would be more important. So many packages require newer versions of Swift that I'm not sure how successful that would be unless there are few dependencies. Almost everything I use needs at least 5.5. If you are distributing on the App Store, App Store Connect will generally only accept one version of Xcode back. There are other reasons you would generally need a recent Xcode for a side-loaded app.

I've been fortunate enough to only need to target latest releases, so I don't have a lot of experience with testing across multiple Xcode or operating system versions. That might be different in non-enterprise environments. We have strict patching requirements for security and Apple doesn't fully patch previous operating systems. Even Xcode often taking a few months to address CVEs (and no back patching) has been a minor issue for us.

I think the biggest issue is needing to run the same version of swift-syntax as everyone else. I know there were proposals, but I think no solution for this made it in to 5.9. Hopefully we see a real solution before the next release of Swift or we will start to get compatibility issues. I've already had to fork several open source macro projects to change the swift-syntax version to play with them together in the same project.

EDIT: Too late for me to be editing this.

That's the main place which I see SwiftMacroToolkit fitting into the picture. At the moment it's not comprehensive, so it does expose quite a bit of SwiftSyntax in most usecases, but with some work it would be possible to make a pretty comprehensive wrapper that completely hides away SwiftSyntax's complexities and breaking changes. One future direction I had in mind was perhaps supplying a set of simpler macro protocols which macro devs can use to write macros completely in MacroToolkit land (and those protocols simply extend the SwiftSyntax protocols with default implementations of the macro methods to bridge the SwiftSyntax arguments to their MacroToolkit counterpoints). For example:

public struct MyMacro: ToolkitPeerMacro {
    // The protocol would add many overloads of expansion and macro devs can
    // decide which ones to implement. The protocol will have a default implementation
    // for regular `PeerMacro`'s `expansion` method which figures out which
    // overload of expansion to call (I have ideas for how, but that's not important). 
    public static func expansion<Context: MacroExpansionContext>(
        // The attribute is passed as a MacroAttribute which has many helpers
        // for destructuring arguments etc
        of attribute: MacroAttribute,
        // Library automatically throws error if macro isn't attached to a struct
        providingPeersOf declaration: StructDecl,
        // Could possibly do something useful with a context wrapper too
        in context: Context
    ) throws -> [DeclSyntax] {
        print("Expanding ", attribute.name)
        print("name argument ", attribute.argument(labelled: "name")!)
        // ...

Unfortunately I'm just too busy to work on SwiftMacroToolkit in any major way at the moment, but I'm open to discussion/contributions.


A bit off topic, but another idea I've had was implementing something like Rust's macro_rules! as a macro which allows devs to concisely create simple pattern matching macros (from my understanding, there's nothing stopping a macro dev from using macros from another library to implement their own macros).

3 Likes

I think you misunderstand what the problem is. They have explicitly stated that they intend to bump the major version of swift-syntax with every minor version of Swift: Swift 5.9 swift-syntax is 509.0.0, and a hypothetical 5.10 would be 510.0.0. This means that a package which depends on 509 and a package which depends on 510 are incompatible with each other, and cannot appear in the same dependency graph. Even if your macro does not care about any 5.10 features, it needs to upgrade to 510 or it blocks other macros from updating to 510.

The Swift language version used when actually compiling things is not related to this. It's purely a SPM package version resolution problem.

6 Likes

Yeah. It feels like even if their goal is zero source breakage it will happen eventually. Swift 6 or 7 will come out and they will want to implement some new feature in swift-syntax that breaks an older language mode. I agree that there needs to be a solution. We are probably good until at least Swift 6 without a real plan to tackle this, but it would be nice to at least see interest in forming a plan.

I'm sure if Swift somehow fails to come up with a plan the community will, but that isn't a great look for something that feels like a core feature. Maybe even Pointfree could make it their next series to fix the limitations of Swift's Macro story much like they did with unit testing.

My personal opinion is that SPM should allow different dependencies for different macros or build plugin targets. It doesn't feel that should be too hard to implement. It doesn't solve long compilation times... but at least your code compiles.

I'm pretty sure this was brought up in the pitches for macros, but I don't recall if it was thrown out for any particular reason.

1 Like

Maybe swift-syntax could be provided as a binary artifact for all supported build platforms as another option, but that certainly feels less than ideal. It would at least help with compile times which might be worth it.

As a side note, this release removed the ConformanceMacro and introduced the ExtensionMacro, which will break any conformance macro that updates their packages. Should we hope for beta 6 to drop today to address the issue?

Edit: And there it is. :slight_smile:

5 Likes

A relatively straightforward approach which doesn't require new SPM functionality or for swift-syntax to change how it's versioned would be for each major version of swift-syntax to be published as a separate project. Rather than depending on version 509 of GitHub - apple/swift-syntax: A set of Swift libraries for parsing, inspecting, generating, and transforming Swift source code., you'd depend on github.com/apple/swift-syntax-509 and import SwiftSyntax509. This would allow mixing SwiftSyntax509 and SwiftSyntax510 within a single dependency graph because they're just two separate libraries.

This could also be done by just forking swift-syntax ourselves, but that has the downside of that if two libraries you depend on do that then you end up building swift-syntax twice even if they use compatible versions, and as noted swift-syntax takes a long time to build.

5 Likes

One thing I would like to clarify, is that the version of swift-syntax is independent of the version of Swift. It is possible to mix-and-match swift-syntax and Swift compiler versions, i.e. swift-syntax 509 will also work with a Swift 5.10 compiler and a Swift 5.9 compiler works with swift-syntax 510 because swift-syntax is just an ordinary package dependency. So, updating swift-syntax is independent from updating the compiler. Unless your library is relying on interpreting cutting edge language features inside the macro, I suspect that it should work equally well with an older version of swift-syntax.

Some comments seem to have mixed these two up. To avoid confusion I would suggest that we use the terms “Swift Compiler” and “swift-syntax” to refer to the different components.

That also explains why language modes are irrelevant here. Language modes are a compiler feature, not a swift-syntax feature (again: think of swift-syntax as any other package dependency that doesn’t care about a language mode either).

Good suggestion, done. Prune example projects that don’t use swift-syntax 508.0.0 by ahoppen · Pull Request #2033 · apple/swift-syntax · GitHub

Do you refer to Swift compiler or swift-syntax versions here?

I just want to highlight that this is a very good summary! Thanks for writing it.

11 Likes

I want to highlight this, as it seems to me to be the only really viable solution until SPM may be changed in the indefinite future.

I’d like to see Apple take a leadership role here towards addressing what is a very big problem, and commit to setting up and maintaining these named swift syntax projects, and advocating and modeling their use.

1 Like

I guess I'm confused why swift-syntax uses 509.0.0 for Swift 5.9. If semantic versioning is desired as to not break compatibility (not as often anyway) between packages using Macros, wouldn't it be better to version like this: 1.509.0

EDIT: If this semantic version change is hard to do, then maybe move to a new repository such as "apple/swift-macros".

1 Like

There's an underscored version of #if canImport that supports module version checks. Maybe @NeoNacho can clarify its current state, but I reckon it could help with SwiftSyntax adoption somewhat in the meantime.

4 Likes

If swift-syntax is like any other packages and doesn't require the same Swift version, doesn't it make sense to have independent versioning for swift-syntax? If swift-syntax is independently versioned, then macro packages don't have to manage versioning themselves.

1 Like

Swift tools version, so effectively the compiler (or minimum supported compiler).

Assuming swift-syntax 510 does not use Swift 5.10 features?

I think the main concern still highlighted in this thread is that depending on multiple packages that use macros will quickly get to an unresolvable state without further guidance.

2 Likes