Contributing to Swift — Part 2

In part 1 of the blog post, you learned about the basics of contributing to Swift — finding issues to resolve, checking out the source code, etc.

In this final part, you will learn how I resolved 5 issues & got 4 pull requests merged into the compiler and most importantly, you will learn a bit about how the code that you write every day actually works and how the tools you work with understand and analyse it.

So, I chose 5 issues to resolve:

  1. SR-9213: Incorrect error message when trying to use keypath with tuple (merged, available in Swift 5)
  2. SR-9218: Extending a protocol with self constraint to the same protocol should warn (merged, available in Swift 5)
  3. SR-8434 (and SR-8435 — related): (1) Don’t require a return in functions that take an uninhabited type and (2) Warn if there’s code in a function that takes an uninhabited type (merged, will be available in a later release of Swift 5)
  4. SR-9391: Incorrect fix-it when trying to use a selector inside an extension of a non-objc protocol (merged, will be available in a later release of Swift 5)

Let’s walk through how I resolved these issues:

Issue #1: Incorrect error message when trying to use keypath with tuple

Currently, Swift KeyPath does not work with tuples. The compiler was throwing an incorrect error message when trying to diagnose the use of tuples with KeyPaths:

Swift Playgrounds

As you can see above, the tuple baz has a member named fizz, however the compiler is throwing an error saying that it doesn’t exist. Wait a minute, it’s right there! Why is the compiler failing to find the type member?

So, long story short, the compiler’s type checker has a function called diagnoseKeypathComponents() in lib/Sema/CSDiag.cpp, which type checks a keypath’s components. As part of the type checking, it performs a lookup (a way for the compiler to match a name to a declaration) on each component and if the lookup fails, then it tries to apply a “typo correction” and if that fails as well, then it emits a “could not find type member” diagnostic error.

The reason why it’s not able to lookup the type member (i.e component) is because KeyPath support for tuples hasn’t been implemented in the compiler’s AST and SILGen libraries (yet 😏).

So how do we fix this issue in the meantime? Well, it’s quite simple — we check if the type is a tuple and if it is, then we emit a custom diagnostic error! Here’s how we do it:

First, we need to add the custom diagnostic error to a file called DiagnosticsSema.def in include/lib/AST, which contains all the diagnostic messages emitted during the semantic analysis phase of the compiler:

  • ERROR is a macro and it defines the type of the diagnostic (there are other diagnostic types, such as WARNING, NOTE and REMARK).
  • expr_keypath_unimplemented_tuple is the identifier for the diagnostic.
  • none is a diagnostic option (we’re passing none because, duh, we don’t want to specify any options).
  • key path support for tuples is not implemented is the diagnostic message.
  • () is an empty tuple, because we’re not passing any values in the diagnostic message (we can specify formats like Identifier or Type in the tuple and pass the values when calling diagnose(). We can then access the values in the message using the % symbol — like %0 or %1).

Then, we can update the code in diagnoseKeypathComponents() to check the type and emit the custom message:

  • currentType is a pointer to the root of the KeyPath expression. currentType->is<TupleType>() checks if the type is a TupleType or not.
  • KPE is a pointer to the KeyPath expression itself. KPE->getLoc() returns the location of the expression in the source file.
  • TC is a reference to the type checker. TC.diagnose() is a function to emit a diagnostic and we pass the location of the KeyPath expression and the name of the diagnostic to it (and yes, it’s auto generated once we add it to the .def file!).
  • isInvalid = true marks the component as invalid.

That’s it! To test it, we can generate our own toolchain and add it to Xcode. You can do that by invoking ./swift/utils/build-toolchain -<prefix> and then copying the toolchain file to:

/Applications/Xcode.app/Contents/Developer/Toolchains/

You can then switch between toolchains by going to Xcode > Toolchains.

Fixed it!

Issue #2: Extending a protocol with self constraint to the same protocol should warn

Swift allows us to add generic constraints to protocol extensions, using the where clause. For example:

Using generic where clauses, we can also do something like:

Swift Playgrounds

Uh oh, something’s wrong! The requirement Self: Foo is redundant, however the compiler does not provide a warning when we add such a constraint. Lets improve where clauses so it can emit a diagnostic when we add redundant constraints like the one above!

The first step is understanding how generics work in the Swift compiler. For the sake of brevity, we’re not going to discuss that here (although here’s an interesting talk from last year’s LLVM conference about how its implemented). Instead, we’ll skip to a file called TypeCheckGeneric.cpp, located in lib/Sema, which is responsible for type checking generic functions, types and parameters.

Inside the file, there’s a function called checkGenericParamList, which type checks generic parameters. When it has finished type checking the parameters, it visits all the requirements introduced using the where clause by calling visitRequirements and checks if all those requirements are valid, and if it is, then it adds those requirements to the GenericSignatureBuilder (a class responsible for constructing a canonical and minimised generic signature from a list of requirements) passed into the function.

So, to emit a warning, we need to perform an additional check before the requirement is added to the builder. You might ask “Why not perform the check in visitRequirements() instead?” and it’s because the type checker calls it various times for each requirement and can cause our intended warning to be emitted multiple times.

First, we need to add the warning itself to the DiagnosticsCommon.def file:

This looks similar to the diagnostic we discussed before, except one notable change: when we call diagnose(), we also pass the type of the extension, Self and the requirement (as StringRef or string value), which we can then access using %0, %1 and %2 respectively.

Before we write some code, it’s a good time to talk about types, specifically — existentials types. If you’re familiar with type theory and/or predicate logic, you already know what an existential (type) is. If not, then you must be wondering: “What the heck is an existential type?”. Well, here’s an example of an existential type:

Looks complicated? Let’s simplify it into something that will be a little more familiar:

An existential type is a type that lets you “acknowledge” that there exists a type, without specifying what exactly it is. Protocols, protocols with associated types and protocol compositions are all existential types.

Now, to emit the warning, we first have to check whether we are extending an existential type (i.e. a protocol) and then check if both sides of the requirement are the same or not. Here’s how we do it:

  • First, we get the declaration from the owner’s (the extension) declaration context and try casting it to an ExtensionDecl.
  • Then, we get the extension type, the extension’s Self type and the types of both sides of the requirement (req). Remember, Self: Foo is the requirement in the example.
  • Then, we check (a) if the extension’s type is an existential, (b) if the left side of the requirement has the same type as the extension’s Self type (since the left side of the requirement is generic i.e. Self, we cannot directly compare it with the extension’s type, hence why we saved the extension’s Self type earlier) and (c) the right side of the requirement has the same type as the extension’s type.
  • Finally, if the conditions are true, then we emit a warning and pass the types (as string) as an argument to diagnose().
Nice one!

Issue #3: Don’t require a return in functions that take an uninhabited type and warn if there’s code in a function that takes an uninhabited type

So, this one was a bit challenging to solve compared to the rest of the issues, even though the implementation is fairly simple. Long story short, in order to resolve this issue, you have to understand how a type-checked AST (Abstract Syntax Tree) is lowered to SIL (Swift Intermediate Language). To keep things simple, we will not be discussing that here (read this doc for an intro). But what we will be discussing is — uninhabited or bottom types.

An uninhabited type (also known as a bottom type in type theory — I will use these terms interchangeably) is a type that has no values (i.e. it’s empty). It has some special properties, namely:

  • It is a subtype of all types (but the inverse isn’t true — a subtype of all types is not necessarily a bottom type). Since it is impossible for a type to be a subtype of all types, you cannot have an instance of a bottom type, hence why it’s also known as an “uninhabited” or empty type.
  • A function whose return type is a bottom type cannot return.

In Swift, we already have our very own bottom type (with some restrictions): Never — although you can also define your own if you want.

There are many functions that take advantage of a bottom type, such as fatalError(), which has a return type of Never (thus allowing you to, for example, exit a guard statement where you’re expected to throw or return).

We can define a function in Swift that, instead of returning Never, it accepts Never. However, you will notice something very interesting:

Swift Playgrounds

The compiler is complaining that we’re missing a return statement in foo(). Now, if your function declares that its return type is an Int, then there must be a return statement in it that returns an Int, right? Well, yeah.

However, this is a special case — it’s impossible to return from foo()! This is because baz has an uninhabited type — if you passed in a function whose return type is uninhabited, such as fatalError(), then that function will never return and as a result, the code inside foo() will never be executed. This is the same with the functions fizz() and fizzBuzz() declared above — they can never return and none of the code inside their bodies will ever be executed.

So, the requirement to have a return inside foo is redundant, since it will never return and any code you write inside foo() will never be executed. Also, if you haven’t already noticed, while the compiler complains that foo() is missing a return statement, it doesn’t complain that fizz() and fizzbuzz() will never be executed (technically, it does emit a warning when you invoke these functions, but not when you declare them).

So, it’s time to add some new features to the compiler:

  • The ability to declare a function that (1) accepts an uninhabited type, (2) has a return type and (3) has an empty body (i.e no return statement). No warning will be emitted in this case.
  • Emit a warning when declaring a function that (1) accepts an uninhabited type, (2) has a return type (optional) and (3) has a non-empty body. Also, emit a note when declaring such a function to tell the user that they can remove the body to quell the warning.

How do we do this? It’s actually very simple — we just need to emit an unreachable at the very start of a function body. This is a terminating SIL instruction that is used to create a boundary after which code is “unreachable” or not executable.

If we emit this at the start of a function body, then it has two effects — we no longer need to write a return if the body is empty and if the body is not empty, then the compiler will automatically emit a warning saying the code in the body is unreachable.

As mentioned earlier, understanding how the type-checked AST is lowered to SIL is beyond the scope of this article, so we will skip straight to a class known as SILGenProlog, in lib/SILGen.

SILGen is a library that is responsible for lowering a type-checked AST to Swift Intermediate Language (SIL). SILGenProlog is a class which is responsible for doing tasks before a function’s body is emitted, such as creating local arguments that binds to function arguments, emitting an implicit self argument, and more. As you may have guessed, there’s also a corresponding class called SILGenEpilog, which is responsible for doing tasks before a function terminates, such as emitting a return statement.

Inside SILGenProlog, there is a function called emitProlog() that emits a prolog for a given closure (remember, functions are a special case of closures). This is where we’re going to emit an unreachable:

  • First, before emitting the closure’s body, we check if the closure has any parameters. If it does, then we loop over all the parameters and check if its type is uninhabited.
  • If the type is uninhabited, then we create a SILLocation of the parameter, which refers to its (local) location. We then mark this location as being part of the function’s prologue. Then, we emit an unreachable SIL instruction at that location. Note: as mentioned earlier, we’re doing this before we emit the closure’s body, so it doesn’t matter whether it’s the first or last parameter that is uninhabited.
  • Finally, we break out of the loop so the unreachable is only emitted once.

Now, there’s a rule that notes can only be emitted following a warning/error. Unfortunately, the warning for unreachable code in a closure body is not emitted here.

So, in order to emit a diagnostic note, we have to skip to a different file, called SILGenStmt.cpp in lib/SILGen. This class is responsible for lowering statements (such as a guard statement or a switch statement) to SIL. Inside this class, there is a function called visitBraceStmt(), which emits SIL code for each statement inside a brace-enclosed sequence of expressions, statements or declarations, such as { let pi = 3.14; print(pi) }. This is where we are going to emit a note, right after the compiler emits an unreachable code diagnostic if the sequence is unreachable.

  • First, we check if the brace statement (S) has any elements (i.e. statements) or not.
  • If it does, then we get the function which contains this brace statement, get its arguments and then loop over it.
  • If an argument’s type is uninhabited, then we emit a custom diagnostic note (defined in DiagnosticsSIL.def — you probably know by now how diagnostics are defined) and pass it the starting location of the statement body along with the name of the argument.
  • We then break out of the loop so the note is only emitted once.
Smashed it!

Issue #4: Incorrect fix-it when trying to use a selector inside an extension of a non-objc protocol

This one was easy to resolve, however there was a fair amount of discussion around it to decide what’s the best approach while dealing with it.

Swift Playgrounds

In the above example, fizzBuzz cannot be used as a selector because it is not exposed to Objective-C. In order to expose it to Objective-C, you will need mark the entire protocol as @objc.

However, the fix-it provided by the compiler marks the declaration of fizzBuzz in the protocol as @objc, and not the protocol itself.

Uh oh!

This is wrong, as explained by the diagnostic message. Now, that’s an easy thing to fix, however, doing so may cause other issues — for example, if the protocol has associatedtype constraints or if the protocol uses non-bridgable Swift types, then the fix-it won’t work.

It’s not too difficult to check for all the narrow cases where things could go wrong, but doing so adds additional complexity to the compiler and it may not even be the right fix-it depending on how the user is using the protocol.

So how do we fix it (pun intended) then? Well, just don’t provide the fix-it!

In order to do so, we need to visit a class known as CSApply.cpp, located in lib/Sema. This is a class which applies the solution to a set of constraints to an expression, thus resulting in a type-checked expression (read this doc if you want to understand how the constraint system works).

In this class, there is a function known as visitObjcSelectorExpr, which rewrites (i.e. applies the solution to the constraints) an ObjCSelectorExpr (a class that describes an Objective-C selector).

This is where we can add a check to see if the method we’re trying to use as a selector is declared inside a protocol and if it is, we’re going to emit a custom diagnostic error that’s similar to the original, minus the fix-it — and we can do this right before the fix-it diagnostic is emitted.

  • First, we check if the method’s (foundDecl) declaration context is a protocol (i.e. whether it’s declared inside a protocol) by casting it to a ProtocolDecl. If you’re confused as to why we’re checking if the context is a protocol and not a (protocol) extension — this is because we’re referring to a default implementation of the method and hence the declaration context is going to be a protocol (and not an ExtensionDecl for example).
  • If it is, then we simply emit our custom diagnostic & return the selector expression (E).
Hooray!

I hope the blog posts provided you with the motivation that you were looking for in order to get started or helped you along your way to contributing for the first time. If you’re still wondering whether you should start contributing or not, then I highly recommend that you do. Not only will you gain a practical engineering experience (that’s hard to find in smaller, less intricate systems or apps), but you will also gain an understanding of how the code that you spend so much time writing works under the hood, which will allow you to write code that does exactly what you intend it to do.

Your contribution will also help the rest of the Swift community!

PS: Do you enjoy solving problems like above? We’d love you to be a part of our iOS team. Please send me a message if you are interested! 😄

--

--

At Kin + Carta, we're busy building technology experiences for a world where mobile is an expectation, not a device. ‘Kin + Carta Created’ is where we share selected learnings and highlights. Head to www.kinandcarta.com for more.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store