Swift ExpressibleBy protocols: What they are and how they work internally in the compiler - SwiftRocks

Swift ExpressibleBy protocols: What they are and how they work internally in the compiler

ExpressibleBy represents a series of protocols in the Swift Standard library that allows you to instantiate objects directly from token literals, like a string, a number, a floating-point and so on, if the object can be "expressed" like that. For example, here's the regular way of creating an URL in Swift:

func getURL() -> URL
    return URL(string: "https://swiftrocks.com")!
}

However, to prevent having to use this initializer everytime, you could say that it's possible to represent an URL directly from its URL string using ExpressibleByStringLiteral:

extension URL: ExpressibleByStringLiteral {
    public init(extendedGraphemeClusterLiteral value: String) {
        self = URL(string: value)!
    }

    public init(stringLiteral value: String) {
        self = URL(string: value)!
    }
}

This allows us to refactor getURL() to create an URL using nothing else but a string token:

func getURL() -> URL
    return "https://swiftrocks.com"
}

The standard library contains the following ExpressibleBy protocols:

* ExpressibleByNilLiteral: Expressible by nil.
* ExpressibleByIntegerLiteral: Expressible by a number token like 10.
* ExpressibleByFloatLiteral: Expressible by a floating-point token like 2.5.
* ExpressibleByBooleanLiteral: Expressible by true/false.
* ExpressibleByUnicodeScalarLiteral: Expressible from a single unicode scalar. Usage examples of this are Character and String.
* ExpressibleByExtendedGraphemeClusterLiteral: Similar to UnicodeScalar, but consists of a chain of scalars (a grapheme cluster) instead of a single one.
* ExpressibleByStringLiteral: Expressible by a string token like "SwiftRocks".
* ExpressibleByArrayLiteral: Expressible by an array token like [1,2,3].
* ExpressibleByDictionaryLiteral: Expressible by a dictionary token like ["name": "SwiftRocks"].

To make it short, you can use these protocols to hide unnecessary implementation details and possibly ugly initializers of your more complex types. An example use case is how Apple's SourceKit-LSP uses them to represent arbitrary arguments -- because the Any type does not conform to Codable, a CommandArgumentType enum is used to represent unknown arguments:

public enum CommandArgumentType: Hashable, ResponseType {
  case null
  case int(Int)
  case bool(Bool)
  case double(Double)
  case string(String)
  case array([CommandArgumentType])
  case dictionary([String: CommandArgumentType])
}

However, because we're dealing with an enum, representing an argument will result in not-so-pretty lines of code:

func getCommandArguments() -> CommandArgumentType {
    return .dictionary(["line": .int(2),
                        "column": .int(1),
                        "name": .string("refactor"),
                        "args": .array([.string("file://a.swift"), .string("open")])])
}

Fortunately, we can use ExpressibleBy to provide better looking alternatives to the enum:

extension CommandArgumentType: ExpressibleByNilLiteral {
  public init(nilLiteral _: ()) {
    self = .null
  }
}

extension CommandArgumentType: ExpressibleByIntegerLiteral {
  public init(integerLiteral value: Int) {
    self = .int(value)
  }
}

extension CommandArgumentType: ExpressibleByBooleanLiteral {
  public init(booleanLiteral value: Bool) {
    self = .bool(value)
  }
}

extension CommandArgumentType: ExpressibleByFloatLiteral {
  public init(floatLiteral value: Double) {
    self = .double(value)
  }
}

extension CommandArgumentType: ExpressibleByStringLiteral {
  public init(extendedGraphemeClusterLiteral value: String) {
    self = .string(value)
  }

  public init(stringLiteral value: String) {
    self = .string(value)
  }
}

extension CommandArgumentType: ExpressibleByArrayLiteral {
  public init(arrayLiteral elements: CommandArgumentType...) {
    self = .array(elements)
  }
}

extension CommandArgumentType: ExpressibleByDictionaryLiteral {
  public init(dictionaryLiteral elements: (String, CommandArgumentType)...) {
    let dict  = [String: CommandArgumentType](elements, uniquingKeysWith: { first, _ in first })
    self = .dictionary(dict)
  }
}

Which allows us to rewrite getCommandArguments() with easier to read tokens.

func getCommandArguments() -> CommandArgumentType {
    return ["line": 2,
            "column": 1,
            "name": "refactor",
            "args": ["file://a.swift", "open"]]
}

How it works internally

But how can a token become a full type? As with all compiler magic, we can uncover what's going on by intercepting Swift's compilation steps.

Using the first getURL() method as an example, let's first see how Swift treats ExpressibleBy objects. If we compile the code manually using -emit-sil argument, we can extract the Swift Intermediate Language (SIL) version of the code -- the final compilation step in Swift before LLVM takes the wheel.

swiftc -emit-sil geturl.swift

The output, which I edited to make it easier to read, looks like this:

sil hidden @$s3bla6getURL10Foundation0C0VyF : $@convention(thin) () -> @out URL {
bb0(%0 : $*URL):
  %1 = string_literal utf8 "https://swiftrocks.com"
  // removed: creating a String type from the string_literal
  // function_ref URL.init(stringLiteral:)
  %8 = function_ref @$s10Foundation3URLV3blaE13stringLiteralACSS_tcfC : $@convention(method) (@owned String, @thin URL.Type) -> @out URL
  %9 = apply %8(%0, %6, %7) : $@convention(method) (@owned String, @thin URL.Type) -> @out URL
  %10 = tuple ()
  return %10 : $()
} // end sil function '$s3bla6getURL10Foundation0C0VyF'

Here's what the method is doing:

1: Create a string_literal token
2: Create a String type from the literal
3: Call URL.init(stringLiteral:) with the String
4: Return the URL

As one would expect, the compiler achieves this magic by replacing the String line of code with the relevant ExpressibleBy initializer. Hooray for compiler magic!

Now, to locate where this happens in the compiler, we can grep the Swift source for mentions of "ExpressibleBy", which will point us to several places inside CSApply.cpp. In short, all usages of literals get converted to their ExpressibleBy equivalent, including the "expressibles that are literals themselves" (for example, an Int is itself an ExpressibleByIntegerLiteral). When Swift's type-checker reaches a literal, it gets a hold of an instance of the relevant protocol type and the name of the initializer, which can be determined from the literal we're looking at:

Expr *visitNilLiteralExpr(NilLiteralExpr *expr) {
  auto type = simplifyType(cs.getType(expr));
  auto &tc = cs.getTypeChecker();
  auto *protocol = tc.getProtocol(expr->getLoc(),
                                  KnownProtocolKind::ExpressibleByNilLiteral);
   DeclName initName(tc.Context, DeclBaseName::createConstructor(),
                     { tc.Context.Id_nilLiteral });
 //...
}

With that info in hand, the type-checker calls convertLiteralInPlace to replace the full expression with the equivalent ExpressibleBy initializer. The method itself does a lot of stuff, but there's something interesting to note here: If we take a look at KnownProtocols.def, we can see that all literals have default types:

EXPRESSIBLE_BY_LITERAL_PROTOCOL(ExpressibleByArrayLiteral, "Array", false)
EXPRESSIBLE_BY_LITERAL_PROTOCOL(ExpressibleByBooleanLiteral, "BooleanLiteralType", true)
EXPRESSIBLE_BY_LITERAL_PROTOCOL(ExpressibleByDictionaryLiteral, "Dictionary", false)
EXPRESSIBLE_BY_LITERAL_PROTOCOL(ExpressibleByExtendedGraphemeClusterLiteral, "ExtendedGraphemeClusterType", true)
EXPRESSIBLE_BY_LITERAL_PROTOCOL(ExpressibleByFloatLiteral, "FloatLiteralType", true)
EXPRESSIBLE_BY_LITERAL_PROTOCOL(ExpressibleByIntegerLiteral, "IntegerLiteralType", true)
EXPRESSIBLE_BY_LITERAL_PROTOCOL(ExpressibleByStringInterpolation, "StringLiteralType", true)
EXPRESSIBLE_BY_LITERAL_PROTOCOL(ExpressibleByStringLiteral, "StringLiteralType", true)
EXPRESSIBLE_BY_LITERAL_PROTOCOL(ExpressibleByNilLiteral, nullptr, false)
EXPRESSIBLE_BY_LITERAL_PROTOCOL(ExpressibleByUnicodeScalarLiteral, "UnicodeScalarType", true)

This means that if the expression has no type or has a type that doesn't conform to the protocol, the literal's true type will be assigned to the default's type conformance instead. For example, if I removed the conformance for getURL(), the SIL code will reveal that the internal String initializer is used instead:

func getURL() -> URL {
    return String.init(_builtinStringLiteral: "https://swiftrocks.com")
}

This not only allows you to write untyped expressions like let foo = "bar", but it also serves for UI reasons - thanks to that, in a later pass the previous getURL() example will result in our user-friendly Cannot convert value of type 'String' to specified type 'URL' compilation error.

Follow me on my Twitter (@rockthebruno), and let me know of any suggestions and corrections you want to share.

References and Good reads

The Swift Source Code

Contact Info

Bruno Rocha is an iOS Software Engineer and is the developer of open sources libraries like SwiftInfo and SwiftShield.
bruno@swiftrocks.com

Newsletter

Click here to subscribe to my newsletter to get notified of new posts by e-mail.

RSS / Social

Info

This website's static HTML pages are generated by WriteIt.