Why does the Swift compiler continue optimizing SIL even after SIL verification failures?

Hi all,
I'm looking into a compilation bug (SIL verification failure when evaluating `$0` in certain closures. · Issue #66312 · apple/swift · GitHub) in the Swift compiler. The issue is happening due to SIL verification failure during high-level optimizations. I was able to bisect the optimization passes to determine the first one that produces invalid SIL, using the -Xllvm -sil-opt-pass-count CLI option. But I noticed that if I don't use that option the compiler keeps optimizing way beyond the offending optimization pass, before eventually crashing with an error backtrace.

Why?

1 Like

Normally we only run the SIL verifier at the end of the mandatory and optimization pipelines. You can pass the -Xfrontend -sil-verify-all flag to have it run after every pass, so that we'll stop right at the first pass that introduces a verifier error.

2 Likes

Makes sense, thank you! I have a follow up question though.

I was actually trying to bisect the optimization passes to find the offending pass and that is when I saw this issue. However, it also looked like the SIL verification was happening in the same sequence as the optimization passes.

Is that true? If yes, how does the verifier have access to the SIL produced after each transform in different optimization passes? Surely, we must not be storing the intermediate SIL generated after each transform across all optimization passes, since there can be tens of thousands of them.

The verifier is a pass that can be inserted multiple times into the pipeline. We don't have to save the SIL aside.

It verifies the SIL after each pass runs, not after each individual transformation done by each pass. (Passes aren't necessarily written in a way that individual transformations can be staged that way anyway.)

1 Like

So I can see that one SIL optimization pass can perform multiple transformations. Are "transformations" here actually loop passes in some fixed-point optimization algorithm? If yes, what are the sub-passes specified here?

The architecture of the SIL optimizer is a sequence of "passes": independent bits of code that each do their own traversal over function bodies computing some specific analysis or applying some specific class of transformations. For example, there's a pass that just does inlining.

On the one hand, this kind of architecture is known to be prone to pass-ordering problems where it sometimes doesn't optimize things that a more flexible architecture might. On the other hand, it is usually a lot more predictable, easier to debug, and easier to time-bound than the sort of architecture you're talking about where arbitrary transformations are interweaved.

I'm sorry, I think my question was not very clear. I actually have a fairly simple question. I want to understand how to interpret the following output, that I get when I compile my swift source with the -Xllvm -sil-opt-pass-count=13669 flag.

*** SIL function before  #13668, stage HighLevel,Function+EarlyLoopOpt, pass 15: SILCombine (sil-combine)

What is the relation between 13668 and 15 here?

Optimizations like SILCombine and SimplifyCFG can have multiple transformations. Typically, once you have bisected the verifier error to a pass like SILCombine or SimplifyCFG, you can further bisect which specific transformation caused the error with the "sub pass count".

2 Likes

For bisecting, only "13668" matters. "15" here is just the pass index in the pass pipeline table.

1 Like