although not directly addressing the bulk of the problem, Swift 6 does include two peripheral improvements relevant to this thread:
swift-testing now ships with the toolchain, so projects will no longer be exposed to swift-syntax simply by depending on Testing
it is now possible to cross-compile projects that use macros (either directly or more often, through a dependency). although this still involves compiling swift-syntax from source, previously it was not possible to compile such projects at all.
Something impactful here might be a potential tool for benchmarking building times specifically for macro expansion implementations. This could abstract away the noise from swift-syntax and give engineers a chance to focus on optimizing the code they can control (their own macro expansion algorithms).
On that topicā¦ we haven't heard (or seen) much on optimizing macro expansion algorithms (not that I'm aware of). On the "make it compile, make it correct, make it fast" chart the community knows how to make "correct" macro expansion algorithmsā¦ but I'm not sure I'm seeing too much guidance or deep-dives into the performance of macro compilation (and the friction-slash-noise associated with building swift-syntax might be a factor blocking engineers on that).
If you build with -stats-output-dir <path> (make sure the directory exists first, it won't create it for you), there are a number of timing values in the resulting .json file for macro-related type checker requests:
Some of those (the first two?) should include the time taken the spawn the plugin executable, I believe. If you add that flag to your build and look at the data, does it provide any additional insight? It won't cover the building of swift-syntax or the macro itself, but it would capture the execution time of the macro by processes that are using it.
could somebody remind me again what the actual technical problem is of shipping swift-syntax with the toolchain? I only found vague comments when searching this and related threads, and they were partly engulfed in a bit of a "macOS vs other platforms" skirmish.
especially looking at today's landscape, with swift-foundation and swift-testing shipping to all supported platforms (and things like swiftly making it "just work"): why can't we just have the same for swift-syntax? clearly we (or at least Apple folks) know how to include a pre-built lib in the toolchain.
so, (honest question) what is so hard about this?
is it just a prioritization issue?
I believe the original concern was allowing swift-syntax to ship updates more frequently than official toolchains go out, especially on macOS, but that was before the community understood the full impact of consuming swift-syntax as a package. I doubt anyone would argue for that tradeoff today. The worst part of it, of course, is that Swift and Apple already ship macros as part of the toolchain, so there's some sort of solution out there already. I'm not sure whether those consume swift-syntax from within the toolchain or they just have the precompiled macro plugins that can be run directly, but however it works is not available to anyone else.
At this point I would take any solution that allows us to use a precompiled swift-syntax. Easiest would just be for swift-syntax to ship such an artifact directly, but they've refused.
I just don't see how swift-syntax is any different from say swift-testing.
it should just be there.
you can't (realistically) write macros without it, just as you can't (realistically) write tests without swift-testing (or XCTest ofc).
swift macros could be such a transformative feature, but I understand completely why package maintainers only touch them with a long stick and a hazmat suit... : (
FWIW (@grynspan can correct me if I am wrong) one of the main reasons swift-testing is shipping in the toolchain is at attempt to work around the pain-points of building on a swift-syntax dependency. AFAIK the "north star" solution would be to optimize building swift-syntax to the point that swift-testing no longer needs to ship in the toolchain.
To my knowledge, Swift Testing's dependency on swift-syntax was not the primary driver for including the former in the toolchain. This is a topic that the language steering committee discussed at length, as I understandāand I wasn't in the room for that discussion, so I can't really tell you what was said. I believe @tkremenek or @Ben_Cohen was there, so they could maybe tell you more about it.
One possible future for Swift Testing is that it does move back out of the toolchain. The appeal of doing so is that you don't pay the cost of downloading and installing it alongside the compiler if you aren't going to use it (e.g. you're not actively developing your own Swift code, you're just using code other folks have built.) It also would help with cross-compilation because Swift Testing would be buildable from source for targets where we haven't published an official toolchain.
@Douglas_Gregor was working on a way to cache Swift Testing build products such that typical workflows wouldn't incur any compile-time costs for using it. He may be able to share more information about that idea.
My personal preference (and I really want to emphasize that I'm speaking for myself here, not the Swift project, my employer, or my colleagues) would be to keep it in the toolchains we distribute, but also support it as a package for cross-compilation targets. And that's roughly where we are today with Swift 6: if your target doesn't have a native Swift toolchain and you're cross-compiling, you can still include Swift Testing as a package dependency and everything "just works" (assuming we put all our #if os() clauses in the right places, anyway.)
For anyone arriving at this thread and looking for a solution today for Darwin platforms, @sjavora has come up with a solution at swift-syntax-xcframeworks.
My understanding, and someone correct me if I'm wrong, is that wouldn't work because packages can pick which version of swift-syntax they want to build their macros against, so every toolchain would have to ship with prebuilt swift-syntax libraries for every possible version that packages may choose.
I haven't read this entire thread, but what about caching each built swift-syntax version in a central cache, so they only have to be built the first time and can be reused after that? I just saw that SwiftPM shipped with new --experimental-{,un}install commands earlier this year, which allows you to install executables from Swift packages in your local user cache directory and run them directly.
We could probably reuse that logic to install swift-syntax the first time it's built to a directory versioned by toolchain and swift-syntax tag in that local user cache directory, then reuse those cached libSwiftSyntax.so libraries across all Swift package builds from then on with an --experimental-cache-swift-syntax flag, provided the toolchain and swift-syntax version match.
I see two possible problems with this approach:
Current package macros appear to statically link against swift-syntax, so it would be better to change that to dynamic linking.
Swift-syntax does call some system C APIs in a few places, so if someone were to update their system and those C APIs changed in some way, it could break the prebuilt libraries. Extremely unlikely, but you could add an --experimental-empty-cache-swift-syntax flag to flush the cache for such scenarios.
Perhaps this is what @Douglas_Gregor is already working on for "a way to cache Swift Testing build products such that typical workflows wouldn't incur any compile-time costs for using it" that Jon mentioned a couple weeks ago, but we could try such swift-syntax caching also with SwiftPM.
I've recently been annoyed by this same problem when building the trunk Swift toolchain itself, which rebuilds the same tag of swift-syntax over and over again:
Honestly, to me, this is kind of the bigger issue anyway. Sure, the build times are not ideal, but the version dependency in packages is the more dicey problem. It is very easy to accidentally have two packages in your dependency tree that require incompatible version ranges of swift-syntax.
So, in my head (and maybe I am wrong about this) this would be better:
swift-syntax is part of the toolchain (it's version is tied to the toolchain version - a package doesn't "choose" it's version)
swift-syntax has API stable enough to work across the various swift versions it targets (afaik this is already kind of the case)
macro code can use #if compiler shenanigans to smooth out incompatibilities
Each macro is a completely independent binary, so theoretically there is no reason that using different versions of swift-syntax in the same package graph should be a problem, because there is no need for them to fall into a single mutually compatible range. The problem is that SwiftPM wasn't initially designed with that in mind (there were no host binaries, just target), so the limitation that you couldn't have multiple versions of a package in the same graph wasn't an issue previously.
i agree, many packages do not require a specific swift-syntax version (or version range) because they are targeting a specific feature, only out of an abundance of caution to prevent bitrot.
this caution is not unwarranted - itās pretty common for releases to stop compiling as swift-syntax adds tags, due to the peculiarities of dependency resolution. but it has the terrible externality of pushing the problem of incompatible versions onto users who cannot satisfy mutually-exclusive swift-syntax dependencies.
the right approach is for packages to stop specifying swift-syntax versions entirely - it should just be āwhateverā comes with the Swift toolchain being used.
compiling multiple versions of swift-syntax per build - one for each consumer - is not the answer.
If you do this, how do you prevent a macro in your dependency tree from blocking your build if it isn't updated in a timely manner after a breaking swift-syntax change?
Here's a concrete example: let's say you're using a macro that does something with function bodies that includes operations on if statements. In swift-syntax version N, these were represented as IfStmtSyntax nodes. When if expressions were accepted into the language, swift-syntax version N+1 changed the representation to be IfExprSyntax nodes wrapped in ExpressionStmtSyntax.
If you allow that macro to continue building with swift-syntax version N, it's fine for existing source code that doesn't adopt the new featureāit will parse the source text with its version of the parser, which still uses IfStmtSyntax. It won't be able to handle new if expressions, but that's ok; even if the macro could somehow build successfully with the new version of the AST API through some hypothetical compatibility layer, it would still certainly need modifications to understand how those new nodes fit together.
I'm working on a solution for macro build performance that should help a lot on all host platforms. I've been promising it for a while now on one of the Issues. But I need to check on one more thing before I'm confident it'll work and will present it for review on the forums most likely next week after Canadian Thanksgiving.
The dependency issue is another thing. I have a teammate who's looking at being able to handle multiple versions if their dependency graphs are disjoint. But we don't have a solution for that yet.
I am not saying this is not an issue, but packages declaring their supported version range rigidly, with today's "solution", is basically just as bad.
If a macro says "I only work with 510-600", and another one in your dependency tree says "I need 610 at least my friend", you basically run into trouble before the code even tried to compile.
As long as SwiftPM can't deal with this, I do not know who the "it is good because every macro package can define its own version dependency" story is for. It is clearly is not good.
This is brutal. Incremental compile after incremental compile I see macros completely unrelated to the small code changes made burning nearly 100% of the CPU for a significant amount of time.
@dschaefer2 Is the solution you are working on going to help here?