In the past few years, some components of the Swift compiler have started being implemented in Swift, including:
- The new Swift Driver, which coordinates Swift compilations.
- Parsing of regular expression literals.
- Some new SIL optimization passes.
All of these components are optional for one reason or another. The new Swift Driver is optional because we are still maintaining the existing C++ Driver implementation, which can be used for building a compiler with a host that doesn’t support Swift. Regular expression literals and the new SIL optimization passes are optional because we can build a compiler without them, then use that compiler to build a new compiler with them. All of this means that it is still possible to build a (mostly) working Swift compiler on a host where there is no existing Swift compiler, using the host C++ compiler.
I propose that we start requiring an existing Swift compiler to build the Swift compiler. This opens the door to non-optional (mandatory) parts of the compiler to be implemented in Swift.
Requirements for mandatory Swift code in the compiler
For a mandatory part of the compiler to be implemented in Swift, it must:
- Build with CMake, which will be used when building the compiler.
- Be part of a SwiftPM package, to allow a package-based workflow for most development.
- Build with the current
maincompiler, release branch, and all Swift releases shipped in the last 12 months. For example, at the time of this writing,
mainwill become Swift 5.8, and the release branch is Swift 5.7. A mandatory part of the compiler implemented in Swift would have to build with Swift 5.5, 5.6, 5.7, and 5.8. This gives ample time for anyone who wants to work with the Swift compiler to update their host tools (possibly building newer versions) without having to go through a multi-stage bring-up.
- Deploy back to the Swift 5.1 runtime. This would allow Swift Concurrency to be used in the code base (via the back-deployment libraries), as well as opaque result types. However, it would be a significant bump in requirements for using the compiler on macOS: currently, the compiler can run on macOS 10.9 or newer. This would move that requirement to macOS 10.15. Other platforms are unaffected because they don't ship Swift as part of the OS.
- Support cross-compiled builds to other host architectures. For example, this means that the code base must be free of
#if os(...)checks (and similar) that conflate the host and target environments. Once we accept that Swift code can be a mandatory part of the compiler, bringing up a new host environment means cross-compiling all of the compiler’s code.
Use cases for mandatory Swift code in the compiler
The first few candidates for mandatory Swift code in the compiler are:
- The new Swift Driver implementation. The new driver is a standalone replacement for the C++ driver. We can stop building the C++ driver executable, and instead make the new Swift Driver mandatory. As follow-up work,
getSingleFrontendInvocationFromDriverArgumentscan be reimplemented by using the Swift Driver library, allowing the C++ driver to be removed entirely.
- Regular expression literals, which could be enabled all of the time instead of conditionally. This is mostly a simplification to ensure that the language dialect doesn’t depend on how the compiler was built.
- Various uses of gyb and tablegen for code generation could be replaced with Swift code in the build process.
Under this proposal, the new SIL optimization passes are not good candidates for becoming mandatory Swift code. These passes, and indeed the use of SIL instructions from Swift, are being used as testbeds for Swift/C++ Interoperability, so this code cannot become mandatory until Swift/C++ interoperability has been stabilized in a release that is older than the 12-month cutoff.
Concrete Build Process
Here’s a proposed build process for the Swift compiler with Swift code in it:
- Build C++ bits with the host C++ compiler
- Build mandatory Swift bits with the host Swift compiler
- Link a “minimal stage 1" Swift compiler
- Build optional Swift bits with the minimal stage 1 compiler. Note that these bits may not be fully optimized because the stage 1 compiler may lack some optimizer passes.
- Link a “full stage 2” Swift compiler
- Rebuild optional Swift bits with the stage 2 compiler.
- Link a “final stage 3” Swift compiler
The above does create a productivity risk: The optional bits get built twice, and this happens any time the stage 1 compiler changes. In practice, this risks slowing down developers who must wait for additional build steps to get a fully optimized result. Any developer working on the optimizer will need at least the stage 2 compiler; developers interested in compiler performance will need to work with the stage 3 compiler. A workaround would be to have a separate build mode that builds the optional bits with the host compiler; that would provide faster build turnaround for those cases where the host compiler can be fully up-to-date.
Also note: It is possible to do the above with dependency-based build systems (such as Make or Ninja), but it’s tricky to get right. Note that a naive version would have the optional bits depend on both the stage 1 and stage 2 compilers.
There are two cross-compiling scenarios to consider:
- Initial platform bringup: For a new platform, someone will have to cross-build a stage 1 compiler one time and make it available. That is sufficient for everyone involved to do native builds from there on.
- Platforms that are unable to build the compiler natively. This includes targets like Raspberry Pi that are capable of running a Swift compiler but not necessarily building it. These will require a different build process, as the stage 1 compiler above would be for the target platform, not the host.
To ensure that the compiler build succeeds with older compilers, we will need to bring up additional CI to build the
main compiler with all of the supported host compiler versions, e.g., Swift 5.5, 5.6, and 5.7. As new versions of Swift are released, we can drop the CI jobs for older versions when adding the new one. For example, once a new version is released (say, 5.8), the oldest compiler can be removed (e.g., 5.5).
Personally, I'm excited to open the door to having more Swift code in the compiler, but I want to make sure we're doing so in a way that doesn't make it unduly complicated to develop the Swift compiler or port to other host architectures. Thoughts?