Debugging the Swift Compiler, Part 1: Writing Good Bug Reports
For the vast majority of programmers, when our code doesn’t build or doesn’t run correctly it’s almost always our fault. The compiler is pretty much the last thing we blame. However, at PassiveLogic, we push the limits of the Swift programming language, in particular by using an experimental language feature and helping in its evolution. As a result, “my code broke the compiler” is something you hear a little more frequently around here.
One of the awesome things about the Swift language is that its compiler and other components are open source, and anyone can report and even fix issues that they encounter. We’re investing in a team to do just that in support of PassiveLogic’s ecosystem of products that use Swift under the hood. This article is the first in a planned series about the process we use when encountering a bug in the current implementation of the Swift language and how we approach isolating, reporting, and fixing that issue.
Before you can fix a bug, you need to first identify it. Writing a great bug report is an undervalued skill, and can be the difference between a bug sitting there for years or it being fixed in days. The best bug reports make it immediately obvious what’s going wrong, where the problem may lie, and how to verify a fix. Filing an easy-to-reproduce test case for a bug, with a detailed description of how something is deviating from expected behavior, can often be half the battle:
Compiler bugs come in a variety of shapes, but one that Swift developers may have encountered (especially if you worked with the language in its early days or have tried out new features) is the compiler crasher. You try to build some code and the compilation process bombs out with a stack trace at the command line or Xcode gives you a cryptic
Command SwiftCompile failed with a nonzero error code
Let’s say you’ve hit a genuine compiler crasher like this in your code and you want to report it. Before you do so, it’s entirely possible that this bug has already been fixed upstream in the compiler. If you have a project that allows it, try downloading a newer Swift toolchain from Swift.org with the very latest being one of the (nearly) nightly trunk snapshots. Build your code again using that newer toolchain and verify that it still repeats or at least causes a new crasher with that.
If the failure goes away, that’s a strong indication that someone has fixed the problem in the meantime. Fixes and improvements in the main branch of Swift usually appear first in nightly toolchain snapshots on Swift.org, then release branch candidates on Swift.org, and finally the toolchains that ship with versions of Xcode. (We’ll talk in a future article about how to build your own Swift toolchain from scratch if you want the very latest code or want to customize the toolchain for your own needs.)
Assuming the crasher still appears in the latest Swift toolchains, it might be a good idea to check if others have reported it. Before doing that, let’s see what information is provided to us in a stack trace from a typical compiler crasher. This will all be printed to the console when building at the command line or in most build tools, but if you build in Xcode and you only see the above one-line error message, go to the build log and click the detail disclosure icon to show the full stack trace:
Many compiler crashers are due to code leading to unexpected conditions and causing assertion failures. The Swift toolchains that ship with Xcode have many assertions turned off, but the Swift.org toolchains have them turned on. By building with Swift.org toolchains, you may encounter an assertion failure closer to what’s actually going wrong inside the compiler, so that’s another good reason to try your code against one of the Swift.org toolchains.
If you start reading down the stack trace, one of the first things you will encounter is the assertion failure itself, which may look like the following:
Assertion failed: (hasVal), function value, file Optional.h, line 216.
That’s your first clue, and there might be sufficiently distinct wording in the assertion failure to start searching for previously reported issues. Currently, all Swift compiler issues are hosted on GitHub and you can try searching the open issues for the specific assertion failure you’re seeing or for aspects of your code that you think might be related. If someone has reported your exact assertion failure and has reproducible code that is similar to what’s failing locally, great. In our case, we usually don’t find our assertion failures covered in the open issues and we go on to dig a bit deeper.
After the assertion failure message, there’s usually some more interesting information in a stack dump:
1. Apple Swift version 5.9-dev (LLVM f58e4c448dc4009, Swift 07f938435a8b7d1) 2. Compiling with the current language version 3. While evaluating request ExecuteSILPipelineRequest(Run pipelines { Mandatory Diagnostic Passes + Enabling Optimization Passes } on SIL for KeyPathMapCrasher) 4. While running pass #382 SILModuleTransform "Differentiation". 5. While canonicalizing `differentiable_function` SIL node %25 = differentiable_function [parameters 0 1] [results 0] %24 : $@callee_guaranteed (SomeDifferentiableStruct, Double) -> Double // users: %52, %26 6. While ...in SIL function "@$s17KeyPathMapCrasher12someFunctionyyKF". for 'someFunction()' (at /Users/larson/Developer/DifferentiableSwift/CompilerBugs/KeyPathMapCrasher/KeyPathMapCrasher/main.swift:34:8) 7. While processing // differentiability witness for setAndWrite #1 (aStruct:newValue:) in someFunction()sil_differentiability_witness private [reverse] [parameters 0 1] [results 0] @$s17KeyPathMapCrasher12someFunctionyyKF11setAndWriteL_7aStruct8newValueSdAA018SomeDifferentiableJ0V_SdtF : $@convention(thin) (SomeDifferentiableStruct, Double, @guaranteed { var Cacher<SomeDifferentiableStruct, Double> }) -> Double {} on SIL function "@$s17KeyPathMapCrasher12someFunctionyyKF11setAndWriteL_7aStruct8newValueSdAA018SomeDifferentiableJ0V_SdtF". for 'setAndWrite(aStruct:newValue:)' (at /Users/larson/Developer/DifferentiableSwift/CompilerBugs/KeyPathMapCrasher/KeyPathMapCrasher/main.swift:37:5) 8. While generating VJP for SIL function "@$s17KeyPathMapCrasher12someFunctionyyKF11setAndWriteL_7aStruct8newValueSdAA018SomeDifferentiableJ0V_SdtF". for 'setAndWrite(aStruct:newValue:)' (at /Users/larson/Developer/DifferentiableSwift/CompilerBugs/KeyPathMapCrasher/KeyPathMapCrasher/main.swift:37:5) 9. While generating pullback for SIL function "@$s17KeyPathMapCrasher12someFunctionyyKF11setAndWriteL_7aStruct8newValueSdAA018SomeDifferentiableJ0V_SdtF". for 'setAndWrite(aStruct:newValue:)' (at /Users/larson/Developer/DifferentiableSwift/CompilerBugs/KeyPathMapCrasher/KeyPathMapCrasher/main.swift:37:5)
and a stack dump without symbols:
0 swift-frontend 0x000000010575c184 llvm::sys::PrintStackTrace(llvm::raw_ostream&, int) + 56 1 swift-frontend 0x000000010575b528 llvm::sys::RunSignalHandlers() + 112 2 swift-frontend 0x000000010575c7c4 SignalHandler(int) + 304 3 libsystem_platform.dylib 0x000000019a96ea24 _sigtramp + 56 4 libsystem_pthread.dylib 0x000000019a93fc28 pthread_kill + 288 5 libsystem_c.dylib 0x000000019a84dae8 abort + 180 6 libsystem_c.dylib 0x000000019a84ce44 err + 0 7 swift-frontend 0x00000001058a6a30 swift::autodiff::PullbackCloner::Implementation::getAdjointBuffer(swift::SILBasicBlock*, swift::SILValue) (.cold.1) + 0 8 swift-frontend 0x000000010103b428 swift::autodiff::PullbackCloner::Implementation::getTangentValueCategory(swift::SILValue) + 260 9 swift-frontend 0x00000001010389a4 swift::autodiff::PullbackCloner::Implementation::run() + 2280 10 swift-frontend 0x0000000101038084 swift::autodiff::PullbackCloner::run() + 24 11 swift-frontend 0x0000000101054e78 swift::autodiff::VJPCloner::Implementation::run() + 1368 12 swift-frontend 0x0000000101055540 swift::autodiff::VJPCloner::run() + 24 13 swift-frontend 0x000000010118cb00 (anonymous namespace)::DifferentiationTransformer::canonicalizeDifferentiabilityWitness(swift::SILDifferentiabilityWitness*, swift::autodiff::DifferentiationInvoker, swift::IsSerialized_t) + 5616 14 swift-frontend 0x000000010118f674 (anonymous namespace)::DifferentiationTransformer::promoteToDifferentiableFunction(swift::DifferentiableFunctionInst*, swift::SILBuilder&, swift::SILLocation, swift::autodiff::DifferentiationInvoker) + 3924 15 swift-frontend 0x000000010118cd50 (anonymous namespace)::DifferentiationTransformer::processDifferentiableFunctionInst(swift::DifferentiableFunctionInst*) + 468 16 swift-frontend 0x000000010118ad2c (anonymous namespace)::Differentiation::run() + 1380 17 swift-frontend 0x0000000101217968 swift::SILPassManager::runModulePass(unsigned int) + 980 18 swift-frontend 0x000000010121d964 swift::SILPassManager::execute() + 624 19 swift-frontend 0x00000001012148fc swift::SILPassManager::executePassPipelinePlan(swift::SILPassPipelinePlan const&) + 68 20 swift-frontend 0x0000000101214880 swift::ExecuteSILPipelineRequest::evaluate(swift::Evaluator&, swift::SILPipelineExecutionDescriptor) const + 68 21 swift-frontend 0x0000000101253d78 swift::SimpleRequest<swift::ExecuteSILPipelineRequest, std::__1::tuple<> (swift::SILPipelineExecutionDescriptor), (swift::RequestFlags)1>::evaluateRequest(swift::ExecuteSILPipelineRequest const&, swift::Evaluator&) + 28 22 swift-frontend 0x000000010123d16c llvm::Expected<swift::ExecuteSILPipelineRequest::OutputType> swift::Evaluator::getResultUncached<swift::ExecuteSILPipelineRequest>(swift::ExecuteSILPipelineRequest const&) + 252 23 swift-frontend 0x0000000101214af0 swift::executePassPipelinePlan(swift::SILModule*, swift::SILPassPipelinePlan const&, bool, swift::irgen::IRGenModule*) + 84 24 swift-frontend 0x000000010123f210 swift::runSILDiagnosticPasses(swift::SILModule&) + 192 25 swift-frontend 0x0000000100b399a4 swift::CompilerInstance::performSILProcessing(swift::SILModule*) + 68 26 swift-frontend 0x0000000100997ec0 performCompileStepsPostSILGen(swift::CompilerInstance&, std::__1::unique_ptr<swift::SILModule, std::__1::default_delete<swift::SILModule>>, llvm::PointerUnion<swift::ModuleDecl*, swift::SourceFile*>, swift::PrimarySpecificPaths const&, int&, swift::FrontendObserver*) + 792 27 swift-frontend 0x00000001009975b0 swift::performCompileStepsPostSema(swift::CompilerInstance&, int&, swift::FrontendObserver*) + 600 28 swift-frontend 0x00000001009a6944 withSemanticAnalysis(swift::CompilerInstance&, swift::FrontendObserver*, llvm::function_ref<bool (swift::CompilerInstance&)>, bool) + 160 29 swift-frontend 0x000000010099a128 performCompile(swift::CompilerInstance&, int&, swift::FrontendObserver*) + 740 30 swift-frontend 0x0000000100999098 swift::performFrontend(llvm::ArrayRef<char const*>, char const*, void*, swift::FrontendObserver*) + 2504 31 swift-frontend 0x00000001007ec354 swift::mainEntry(int, char const**) + 2144
These have a lot of useful information in them that can help identify where, exactly, the compiler tripped and fell down. The first stack trace tells us in item 6 that the crash occurred when processing someFunction()
at line 34, column 8 of main.swift. That already shows where to start looking in your code.
Within the second stack trace, lines 0-6 can be stepped over because those are all for the supporting code to print the stack trace. The good stuff starts from line 7 down. We can see that getAdjointBuffer()
in PullbackCloner.cpp
is where the compiler encountered the assertion failure. We can also see the entire stack trace of calls in the compiler that led to that. From the perspective of a compiler engineer, the value of this information is apparent and thus hugely helpful in a bug report.
We’ve now learned a bit about what could be going wrong and where, and we can start filing a bug report based on the above, but the very best reports also include easy-to-use steps for reproducing the problem. In the case of a compiler crasher, the reproducer details include the bare minimum code that brings out the crash, ideally within a single file that has no other dependencies.
The best reproducer cases for Swift compiler bugs are single, self-contained files that can be built directly using the command swiftc test.swift
(or swiftc -O test.swift
, if this bug only occurs in optimized builds) and have the absolute minimum amount of code needed to trigger the issue. (Please make sure to remove or anonymize all references to proprietary code in your reproducers before posting them in public.)
A single file is preferred because it is generally easier to debug the compiler running against one file than if you need to use the full Swift Package Manager infrastructure to build a project.
When minimizing a reproducer from a large codebase, I always start with what the compiler stack trace is telling me. In the above example, the stack trace narrows down the failure to the line of code that causes the problem. Take a look at the code around that point. Developers around the world use Swift every day without it crashing, so something about your code has to be different enough to cause the compiler to break. Look for interesting combinations of features (in our case, differentiable Swift’s special valueWithGradient(at:of)
function and throwing functions).
Once you have a general location and what you think might be the items leading to the failure, start ablating off code to narrowly isolate the failure point. Ideally, this will be a quick process and you can pull out a single standalone function and have it still break the compiler. Unfortunately, sometimes it can be a much slower process of isolation. Remove things until the compiler stops breaking, then roll back until it does again and iterate until you’ve sufficiently isolated the problem area.
My coworker Martin is especially talented at tracking down reproducers for Swift compiler bugs and writing up reports on them. His bug reports have led to quick fixes for some longstanding Swift compiler issues. I asked him for some advice on how he approaches minimizing a compiler crasher from a large codebase:
One of the first goals in generating a reproducer from a large codebase is to reduce build times. Because there is a significant amount of trial and error involved in determining which dozen or two lines of code cause the compiler to crash, speeding up each iteration is high on the list of priorities.This process can include removal of entire unit test suites which have nothing to do with the offending code, or preventing the compilation of entire modules that are not involved with the code in question.At this point, it’s useful to use a small utility that simply replaces each non-differentiable Swift function body with a call to
fatalError()
. Once a given file has had its function bodies replaced as such, it will not only further reduce build times, but also highlight code which was not contributing to the crasher in the first place. Once large swaths of the codebase have been processed in this way, we can then begin removing entire structs or classes that are no longer referenced.
Hopefully, at the end of this process, you’ll have a small reproducer that still breaks the latest nightly builds of the compiler. Now it’s time to actually write up your bug report. Head on over to apple/swift’s GitHub issues and create a new issue with a descriptive title. Try to be more descriptive than “compiler crashes with this code” and include some unique details (assertion failure, where the crash happens, etc.). In the body of the bug report, list out the assertion failure, all stack traces, and describe your reproducer such that anyone can copy it and run it locally.
As an excellent example of the kind of bug report that can result from the process described above, I direct you to apple/swift GitHub issue #63331 that Martin filed in January. The assertion failure and stack traces I used as examples above were drawn from that issue, and you can see how he clearly lays out each piece of information and provides a minimal, single-file reproducer case that’s easy to run. Armed with this kind of information, compiler engineers are well-prepared to start diagnosing and then fixing the crasher — that will be the subject of our next posts in the series.