Safely unboxing the Swift language & standard library

Swift Diagnostics: #warning and #error

New diagnostic directives in Swift 4.2. What are they and how are they implemented?

11 June 2018 ∙ Swift Internals ∙ written by Greg Heo ∙ Swift 4.2 Beta

Aside from straight-up code, our source files are filled with two other categories of things: comments and compiler directives.

Comments are great, but we often leave a TODO somewhere and then it stays for all eternity. Sometimes you need a little friction (OK, maybe even a lot) to remind yourself or others to pay attention to certain parts of the code.

New in Swift 4.2 are two handy diagnostic directives that can help: #error and #warning. What do they do and how do they work?

Prepare for a dive into some C++ code as we examine how these directives are implemented. Along the way, we’ll look at the basics of how the Swift compiler works.

Diagnostic Directives

As their names suggest, the two directives will bring up diagnostic messages of different severity levels in the compiler:

That should get the keywords recognized. Next is to parse them, and extract the message portion. We’ll need some kind of storage to hold two bits of information:

Whether it’s a warning or an error

The message string

Declaration

Swift has the concept of declarations to distinguish parts of the language. An import, a variable declared with var or a constant with let, a new class or an extension — these are all different kinds of declarations.

We’ll define a new “diagnostic” declaration with a flag on whether it’s a warning or error, rather than have two separate declarations. Here’s the start of the class definition:

First, we have getKind() to return the correct DiagnosticKind — either error or warning. We already have errors and warnings in the compiler, so DiagnosticKind already exists.

The method here calls through to isError():

boolisError(){returnBits.PoundDiagnosticDecl.IsError;}

What is this Bits thing? Every kind of declaration has 64 bits of space for some flags and metadata. For example, var has a flag on whether the declaration is static or not; enumeration cases have a flag on whether the case has associated values.

Our PoundDiagnosticDecl has a one-bit IsError field defined. If true (1) it’s an error; if false (0) it’s a warning.

TC is the type checker object. You’ve already seen a diagnose() call in the parser, and the type checker has a similar method to raise warnings and errors.

In this case it’s not a diagnostic about the code itself, i.e. there isn’t a type mismatch or a syntax error. Instead, #warning and #error are meant to trigger diagnostics directly, and they use the same diagnose() as would be used for any other error or warning.

Code Generation

In the case of #error, compilation would have stopped already.

For #warning, compilation can continue on to the next phase: code generation. We’ve finished semantic analysis of the AST, and now we need to generate Swift Intermediate Language (SIL) code.

Again, we’re walking down the tree, writing some SIL as we go, and come across a PoundDiagnosticDecl instance. How do we handle it? Take a look at SILGen.cpp:

voidSILGenModule::visitPoundDiagnosticDecl(PoundDiagnosticDecl*PDD){// Nothing to do for #error/#warning; they've already been emitted.
}

Ha! It was a trick question. 😈

We’ve already generated the compiler warning, which was the whole point. There’s no functional difference in the program itself, so there’s no corresponding SIL.

That means it’s the end of the line for our diagnostic directives. If there’s no SIL then there’s nothing to carry on to the Intermediate Representation (IR) and beyond to the final compiled object file.

The Closing Brace

That was a new language feature in a nutshell. For #error and #warning, the steps were:

Add tokens so the new directives are recognized.

Parse the declarations and check for syntax errors.

Store the declarations in the AST

Add handlers for what to do when you come across one of these diagnostic directives.