MattDiephouse

Reviewer of PRs.

Maintainer for Carthage, ReactiveCocoa, ReactiveSwift, PersistDB.

Ex-GitHub, ex-Apple.

I'm passionate about writing great software.

Most Recent

  1. » Value-Oriented Programming
  2. » Eliminating Impossible States with Never
  3. » Better, Faster, Cheaper
  4. » When Not to Use an Enum
  5. » A Smarter Package Manager

All Articles

Entry 7: Member Operator Errors

31 Aug 2018

Swift’s synthesized Equatable and Hashable conformance (SE-0185) is one of my favorite Swift features. I use a lot of value types and use equality extensively for testing.

But there’s one part I don’t love: the errors if one of the types you rely on don’t conform to the requisite protocol.

struct NotEquatable {}

struct B: Equatable {
    let a: NotEquatable
}

Swift doesn’t give you any hints about why it can’t synthesize a == for you. But worse still: it notes every == in the standard library (64 of them!):

error: type 'B' does not conform to protocol 'Equatable'
struct B: Equatable {
       ^
Swift.==:1:13: note: candidate has non-matching type '(Any.Type?, Any.Type?) -> Bool'
public func == (t0: Any.Type?, t1: Any.Type?) -> Bool
            ^
Swift.==:1:13: note: candidate has non-matching type '<T where T : RawRepresentable, T.RawValue : Equatable> (T, T) -> Bool'
public func == <T>(lhs: T, rhs: T) -> Bool where T : RawRepresentable, T.RawValue : Equatable
            ^
Swift.==:1:13: note: candidate has non-matching type '((), ()) -> Bool'
public func == (lhs: (), rhs: ()) -> Bool
            ^
Swift.==:1:13: note: candidate has non-matching type '<A, B where A : Equatable, B : Equatable> ((A, B), (A, B)) -> Bool'
…and so on

I ran into recently at work with some large types and realized that Swift could and should provide something better. I’d like for Swift to say note: cannot synthesize `==` because `NotEquatable` is not `Equatable`. But before doing that, I thought this more general problem should be addressed. (More diagnostics won’t help if they’re lost in the see of non-matching type diagnostics.)

So I jumped in by looking for the source of the existing error. Searching for non-matching in DiagnosticsSema.def quickly turned up the relevant diagnostic. Searching for its ID turned up one match in TypeCheckProtocol.cpp:diagnoseMatch. It wasn’t obvious where this function was getting called from, so I set a breakpoint and ran my test script.

It was called in a loop inside ConformanceChecker::resolveWitnessViaLookup:

// Diagnose each of the matches.
for (const auto &match : matches)
    diagnoseMatch(dc->getParentModule(), conformance, requirement, match);

Those matches were found earlier in the function:

// Find the best witness for the requirement.
SmallVector<RequirementMatch, 4> matches;
unsigned numViable = 0;
unsigned bestIdx = 0;
bool doNotDiagnoseMatches = false;
bool ignoringNames = false;
bool considerRenames =
    !canDerive && !requirement->getAttrs().hasAttribute<OptionalAttr>() &&
    !requirement->getAttrs().isUnavailable(TC.Context);
if (findBestWitness(requirement,
                  considerRenames ? &ignoringNames : nullptr,
                  Conformance,
                  /* out parameters: */
                  matches, numViable, bestIdx, doNotDiagnoseMatches)) {

Since this is C++, matches is returned via an out parameter, which is nicely labeled here. Placing an earlier breakpoint and stepping into findBestWitness led me to WitnessChecker::lookupValueWitnesses, which is responsible for looking up possible implementations for a given protocol requirement.

It, in turn, had code to look up and return the witness for operators:

if (req->isOperator()) {
    // Operator lookup is always global.
    auto lookupOptions = defaultUnqualifiedLookupOptions;
    if (!DC->isCascadingContextForLookup(false))
        lookupOptions |= NameLookupFlags::KnownPrivate;
    auto lookup = TC.lookupUnqualified(DC->getModuleScopeContext(),
                                       req->getBaseName(),
                                       SourceLoc(),
                                       lookupOptions);
    for (auto candidate : lookup) {
        witnesses.push_back(decl);
    }
}

If I could avoid adding irrelevant decls to witnesses, then I would see fewer diagnostics. So how could I exclude decls?

Luckily, I remembered that protocols can only have operators if one of the operands is Self:

protocol B {
    static func +(lhs: Int, rhs: Int) -> String
}

error: member operator '+' of protocol 'B' must have at least one argument of type 'Self'
        static func +(lhs: Int, rhs: Int) -> String
                    ^

That seemed like a good heuristic to use. Most of the ==s I wanted to exclude didn’t use type I was trying to conform to Equatable—which is why they were so bothersome.

So I did what any reasonable programmer would do: I stole code from the source of that error. After looking it up, I found this code in TypeCheckDecl.cpp:

void checkMemberOperator(TypeChecker &TC, FuncDecl *FD) {
  // Check that member operators reference the type of 'Self'.
  if (FD->isInvalid()) return;

  auto *DC = FD->getDeclContext();
  auto selfNominal = DC->getSelfNominalTypeDecl();
  if (!selfNominal) return;

  // Check the parameters for a reference to 'Self'.
  bool isProtocol = isa<ProtocolDecl>(selfNominal);
  for (auto param : *FD->getParameters()) {
    auto paramType = param->getInterfaceType();
    if (!paramType) break;

    // Look through a metatype reference, if there is one.
    paramType = paramType->getMetatypeInstanceType();

    // Is it the same nominal type?
    if (paramType->getAnyNominal() == selfNominal) return;

    if (isProtocol) {
      // For a protocol, is it the 'Self' type parameter?
      if (auto genericParam = paramType->getAs<GenericTypeParamType>())
        if (genericParam->isEqual(DC->getSelfInterfaceType()))
          return;
    }
  }

  // We did not find 'Self'. Complain.
  TC.diagnose(FD, diag::operator_in_unrelated_type,
              FD->getDeclContext()->getDeclaredInterfaceType(),
              isProtocol, FD->getFullName());
}

I copied this over, removed parts I didn’t think I’d need, and changed it to return a bool to say whether it was valid. I then called it in the loop before adding each decl to witnesses.

bool WitnessChecker::isMemberOperator(FuncDecl *decl, Type type) {
  auto nominal = type->getAnyNominal();

  // Check the parameters for a reference to 'Self'.
  for (auto param : *decl->getParameters()) {
    auto paramType = param->getInterfaceType();
    if (!paramType) break;

    // Look through a metatype reference, if there is one.
    paramType = paramType->getMetatypeInstanceType();

    // Is it the same nominal type?
    if (paramType->getAnyNominal() == nominal)
      return true;
  }
  return false;
}

I ran the changes on my test file and it seemed to work! Yay! So now I attempted to run the full test suite with utils/build-script --debug --test.

Unfortunately, that didn’t work: the standard library didn’t compile. Oops. I definitely broke something. But what? It’s hard to diagnose failures in the standard library—particularly when they involve .gyb files.

To work around this, I:

  1. Stashed my changes
  2. Built with utils/build-script to rebuild the standard library
  3. Unstashed my changes
  4. Built just swift with ninja -C path/to/build/dir swift
  5. Ran the tests with lit

That uncovered a bunch of test failures to look through. One case I hadn’t considered was operators in protocol extensions:

infix operator %%%
infix operator %%%%

protocol P1 {
  static func %%%(lhs: Self, rhs: Self) -> Bool
}

extension P1 {
  static func %%%%(lhs: Self, rhs: Self) -> Bool {
    return !(lhs %%% rhs)
  }
}

The check for Self that I’d removed was still needed.

Another was when the operator was generic:

infix operator <~>
protocol P1 {
    static func <~>(x: Self, y: Self)
}

protocol P2 {}

struct ConformsWithMoreGenericWitnesses : P1, P2 {
}
func <~> <P: P2>(x: P, y: P) {}

I was excluding those when I shouldn’t have. My check needed to let generic types through—I wanted to prevent false positives, but I couldn’t have any false negatives.

I also found one test that I couldn’t run directly as a file: it imported something and that import was essential to the test failure. Fortunately, I realized that the test suite prints out the whole swiftc invocation with the test failure. I was able to copy this bit, paste it in my Xcode scheme, and debug the failure in Xcode:

-frontend -target x86_64-apple-macosx10.9 -module-cache-path /Users/mdiep/Repositories/apple/build/Ninja-DebugAssert/swift-macosx-x86_64/swift-test-results/x86_64-apple-macosx10.9/clang-module-cache -sdk /Applications/Xcode10.0.0b6.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.14.sdk -swift-version 4 -emit-silgen -verify-syntax-tree -module-name protocol_resilience -I /Users/mdiep/Repositories/apple/build/Ninja-DebugAssert/swift-macosx-x86_64/test-macosx-x86_64/SILGen/Output/protocol_resilience.swift.tmp -enable-sil-ownership -enable-resilience /Users/mdiep/Repositories/apple/swift/test/SILGen/protocol_resilience.swift

At this point, I opened a PR. Despite my underwhelming PR description, I eventually communicated what I was trying to achieve. Slava Pestov gave me some helpful feedback:

  1. My check for whether the type was generic could be simplified
  2. I hadn’t casted the FuncDecl correctly
  3. He suggested that I try to unify the function I’d copied with its source
  4. He requested some more tests
  5. I’d passed around an object describing the conformance to the protocol, but I didn’t need to

This was all good feedback. And after (1), (3) seemed more doable. Ultimately I ended up with this:

bool swift::isMemberOperator(FuncDecl *decl, Type type) {
  // Check that member operators reference the type of 'Self'.
  if (decl->isInvalid())
    return true;

  auto *DC = decl->getDeclContext();
  auto selfNominal = DC->getSelfNominalTypeDecl();

  // Check the parameters for a reference to 'Self'.
  bool isProtocol = selfNominal && isa<ProtocolDecl>(selfNominal);
  for (auto param : *decl->getParameters()) {
    auto paramType = param->getInterfaceType();
    if (!paramType) break;

    // Look through a metatype reference, if there is one.
    paramType = paramType->getMetatypeInstanceType();

    auto nominal = paramType->getAnyNominal();
    if (type.isNull()) {
      // Is it the same nominal type?
      if (selfNominal && nominal == selfNominal)
        return true;
    } else {
      // Is it the same nominal type? Or a generic (which may or may not match)?
      if (paramType->is<GenericTypeParamType>() ||
          nominal == type->getAnyNominal())
        return true;
    }

    if (isProtocol) {
      // For a protocol, is it the 'Self' type parameter?
      if (auto genericParam = paramType->getAs<GenericTypeParamType>())
        if (genericParam->isEqual(DC->getSelfInterfaceType()))
          return true;
    }
  }

  return false;
}

This worked with the new case I was adding (checking in the context of a specific type’s conformance) but also worked when checking the protocol itself.

Now instead of noting all 64 ==s in the standard library, it only noted 8 that were generic!

The PR is here.

Entry 6: Unexpected Arguments Diagnostic

24 Jul 2018

As promised, I returned to SR-4270. This code used to return a really terrible diagnostic:

let x: Bool = true
enum A {
    case a
    case b
}
func f(_ a: A) {}
let a: A = .a
f(x ? .a(a) : .b)

In Swift 4.2, it returned a much improved diagnostic:

error: member 'a' takes no arguments
f(x ? .a(a) : .b)
       ^~~~

But Jordan Rose suggested that it would be better if it explicitly mentioned enums.

I followed my usual approach for improving diagnostics:

  1. Search DiagnosticsSema.def for the ID of the diagnostic I want to improve
  2. Search the code for that ID
  3. Actually improve the code there

I quickly found the relevant code in CSDiag.cpp:

case CC_GeneralMismatch: { // Something else is wrong.
  // If an argument value was specified, but this member expects no
  // arguments,
  // then we fail with a nice error message.
  if (!candidateArgTy) {
    if (CS.getType(argExpr)->isVoid()) {
      diagnose(E->getNameLoc(),
               diag::unexpected_parens_in_contextual_member, E->getName())
          .fixItRemove(E->getArgument()->getSourceRange());
    } else {
      diagnose(E->getNameLoc(),
               diag::unexpected_argument_in_contextual_member, E->getName())
          .highlight(E->getArgument()->getSourceRange());
    }
    return true;
  }

  return false;
}

This code used 2 diagnostics:

ERROR(unexpected_argument_in_contextual_member,none,
      "member %0 takes no arguments", (DeclName))
ERROR(unexpected_parens_in_contextual_member,none,
      "member %0 is not a function", (DeclName))

Pretty straightforward. If arguments were given, but none were expected, then the compiler either (1) suggests that you remove the parens if they’re empty or (2) points out that it doesn’t take any arguments.

The hardest part of improving diagnostics is always deciding what the improvement should actually be.

Attempt 1

My first thought was to add a check for an enum and add separate diagnostics in that case. I wasn’t sure how to do that, but I started looking through the rest of the method I was in.

I found that several other diagnostics used CS.getContextualType()—including a test that checked what kind of type it was, CS.getContextualType()->is<UnboundGenericType>(). (CS is commonly used as an abbreviation for constraint system within the compiler.)

The contextual type looked like the right thing and it made sense—this was a contextual member lookup. In other words, the compiler had .foo, a member foo, and had to look up what it referred to by using the context—the type wasn’t explicitly given.

So I added 2 new diagnostics:

ERROR(unexpected_argument_in_enum_case,none,
      "enum case %0 takes no arguments", (DeclName))
ERROR(unexpected_parens_in_enum_case,none,
      "enum case %0 is not a function", (DeclName))

And added code that used them:

if (CS.getContextualType()->is<EnumType>()) {
    if (CS.getType(argExpr)->isVoid()) {
      diagnose(E->getNameLoc(),
               diag::unexpected_parens_in_enum_case, E->getName())
          .fixItRemove(E->getArgument()->getSourceRange());
    } else {
      diagnose(E->getNameLoc(),
               diag::unexpected_argument_in_enum_case, E->getName())
          .highlight(E->getArgument()->getSourceRange());
    }
} else {
    if (CS.getType(argExpr)->isVoid()) {
      diagnose(E->getNameLoc(),
               diag::unexpected_parens_in_contextual_member, E->getName())
          .fixItRemove(E->getArgument()->getSourceRange());
    } else {
      diagnose(E->getNameLoc(),
               diag::unexpected_argument_in_contextual_member, E->getName())
          .highlight(E->getArgument()->getSourceRange());
    }
}

That worked!

error: enum case 'a' takes no arguments
f(x ? .a(a) : .b)
       ^~~~

But the code seemed awfully repetitive, so I wasn’t happy with it.

Attempt 2

In Entry 5, I discovered that DescriptiveDeclKinds can be passed to a diagnostic to print a description of the kind of declaration. (I had changed the description of “enum elements” to say “enum case”.) I thought that I might be able to use that here—if I could figure out how to get a DescriptiveDeclKind.

First I updated the original diagnostics:

ERROR(unexpected_argument_in_contextual_member,none,
      "%0 %1 takes no arguments", (DescriptiveDeclKind, DeclName))
ERROR(unexpected_parens_in_contextual_member,none,
      "%0 %1 is not a function", (DescriptiveDeclKind, DeclName))

I knew that I’d have to get this from the Decl because of the method I’d updated previously:

DescriptiveDeclKind Decl::getDescriptiveKind() const

A Decl is a declaration. It refers to the declaration of anything in Swift—a function, a type, an enum case, etc. In this case, the compiler was trying to pick the right declaration. Looking through the method, I found that there would only be one candidate at this point:

// Dump all of our viable candidates into a CalleeCandidateInfo & sort it
// out.
CalleeCandidateInfo candidateInfo(Type(), candidates, hasTrailingClosure,
                                  CS);

// Filter the candidate list based on the argument we may or may not have.
candidateInfo.filterContextualMemberList(E->getArgument());

// If we have multiple candidates, then we have an ambiguity.
if (candidateInfo.size() != 1) {
  SourceRange argRange;
  if (auto arg = E->getArgument())
    argRange = arg->getSourceRange();
  diagnose(E->getNameLoc(), diag::ambiguous_member_overload_set,
           E->getName())
      .highlight(argRange);
  candidateInfo.suggestPotentialOverloads(E->getNameLoc().getBaseNameLoc());
  return true;
}

So I looked through the types to find that CalleeCandidateInfo had a list of UncurriedCandidates (I knew there’d only be 1 at this point) which had a getDecl() method. Bingo!

So now I could get the DescriptiveDeclKind and pass it to my diagnostic:

auto kind = candidateInfo[0].getDecl()->getDescriptiveKind();
if (CS.getType(argExpr)->isVoid()) {
  diagnose(E->getNameLoc(), diag::unexpected_parens_in_contextual_member,
           kind,
           E->getName())
       .fixItRemove(E->getArgument()->getSourceRange());
} else {
  diagnose(E->getNameLoc(), diag::unexpected_argument_in_contextual_member,
           kind, E->getName())
       .highlight(E->getArgument()->getSourceRange());
}

That also worked, but I realized that the diagnostic wasn’t great as it could be—especially if the parens were empty.

error: enum case 'a' takes no arguments
f(x ? .a(a) : .b)
       ^~~~

But I ran the test suite. I wanted to see (1) where these enum diagnostics were tested and (2) what other types of declarations would have this error. I found some enum tests in test/decl/enum/enumtest.swift, but to my surprise (horror?) I found that there weren’t any other test cases.

I thought about it and figured I could get a similar error by adding a static var:

struct A {
  static var a: A { return A() }
}
let _: A = .a()
let _: A = .a(1)

This yielded these errors:

error: static var 'a' takes no arguments
let _: A = .a()
             ^~~~

error: static var 'a' is not a function
let _: A = .a(1)
             ^~~~

The second error made sense to me, but the first didn’t. Of course a static var takes no arguments. It’s not a function.

Attempt 3

This led me to my final attempt.

I decided that there should be just 2 diagnostics:

ERROR(unexpected_arguments_in_enum_case,none,
      "enum case %0 has no associated values", (DeclName))
ERROR(unexpected_arguments_in_contextual_member,none,
      "%0 %1 is not a function", (DescriptiveDeclKind, DeclName))

The distinction between having arguments or no arguments wasn’t important for the actual message—it only mattered for whether the compiler should suggest a fixit to just remove the empty (). If the type was an enum, I wanted a message about associated values; otherwise I wanted a message about not being a function.

So I updated the code I’d written to use these:

bool isEnum = CS.getContextualType()->is<EnumType>();
bool isVoid = CS.getType(argExpr)->isVoid();
auto argumentRange = E->getArgument()->getSourceRange();
if (isEnum) {
  if (isVoid) {
    diagnose(E->getNameLoc(), diag::unexpected_arguments_in_enum_case,
             E->getName())
        .fixItRemove(argumentRange);
  } else {
    diagnose(E->getNameLoc(), diag::unexpected_arguments_in_enum_case,
             E->getName())
        .highlight(argumentRange);
  }
} else {
  auto kind = candidateInfo[0].getDecl()->getDescriptiveKind();
  if (isVoid) {
    diagnose(E->getNameLoc(),
             diag::unexpected_arguments_in_contextual_member, kind,
             E->getName())
        .fixItRemove(argumentRange);
  } else {
    diagnose(E->getNameLoc(),
             diag::unexpected_arguments_in_contextual_member, kind,
             E->getName())
        .highlight(argumentRange);
  }
}

This seemed good. It provided the diagnostics I wanted. I ran the test suite and all tests passed. So I set to work adding some tests for the static var case.

I looked through the other diagnostics in this case to find where they were tested. Then I’d have a good spot to add my additional tests. I found some code in test/Constraints/diagnostics.swift and updated it with my test:

enum Color {
  case Red
  …
  static var svar: Color { return .Red }
}

…
var someColor : Color
…
someColor = .svar() // expected-error {{static var 'svar' is not a function}}
someColor = .svar(1) // expected-error {{static var 'svar' is not a function}}

I reran these tests, expecting to see them pass. But they didn’t.

test/Constraints/diagnostics.swift:456:41: error: incorrect message found
someColor = .svar() // expected-error {{static var 'svar' is not a function}}
                                        ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
                                        enum case 'svar' has no associated values
test/Constraints/diagnostics.swift:457:42: error: incorrect message found
someColor = .svar(1) // expected-error {{static var 'svar' is not a function}}
                                         ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
                                         enum case 'svar' has no associated values

This was a lucky find. I had been testing with a static var on a struct. The test I added used a static var on an enum. That meant that the contextual type was an enum, so I was using the enum diagnostic. But that was wrong: the contextual type here was an enum, but the member was a static var.

Instead, I should have been testing whether the declaration was an enum element. Luckily, I already had the DescriptiveDeclKind. So this was an easy fix.

auto kind = candidateInfo[0].getDecl()->getDescriptiveKind();
bool isVoid = CS.getType(argExpr)->isVoid();
auto argumentRange = E->getArgument()->getSourceRange();
if (kind == DescriptiveDeclKind::EnumElement) {
  if (isVoid) {
    diagnose(E->getNameLoc(), diag::unexpected_arguments_in_enum_case,
             E->getName())
        .fixItRemove(argumentRange);
  } else {
    diagnose(E->getNameLoc(), diag::unexpected_arguments_in_enum_case,
             E->getName())
        .highlight(argumentRange);
  }
} else {
  if (isVoid) {
    diagnose(E->getNameLoc(),
             diag::unexpected_arguments_in_contextual_member, kind,
             E->getName())
        .fixItRemove(argumentRange);
  } else {
    diagnose(E->getNameLoc(),
             diag::unexpected_arguments_in_contextual_member, kind,
             E->getName())
        .highlight(argumentRange);
  }
}

I tested again to verify that it worked as expected. Then I was able open my PR. Diagnostic improved!

I still have a lot to learn about the Swift compiler, but I’m starting to feel comfortable making improvements to diagnostics. That’s pretty exciting to me: I feel passionate about improving these.

If you’re interested in contributing to Swift, please try improving a diagnostic! Hopefully what I’ve written on this blog can help. But if you’re stuck, feel free to reach out to me or join the (very unofficial) Swift Contributors Slack.

Entry 5: Enum Element Diagnostics

16 Jul 2018

As I look for Swift bugs to work on, I’ve been testing bugs to see if they’re still reproducible. Many of them aren’t—particularly old type-checker issues—so they can be closed.

Take SR-4270 as an example.

let x: Bool = true
enum A {
    case a
    case b
}
func f(_ a: A) {}
let a: A = .a
f(x ? .a(a) : .b) // .a(a) is wrong. A.a takes no arguments.

This used to produce a terrible diagnostic:

error: result values in '? :' expression have mismatching types '' and ''
f(x ? .a(a) : .b)
~~~~~ ^ ~~

But in Swift 4.2, the diagnostic is much better:

error: member 'a' takes no arguments
f(x ? .a(a) : .b)
       ^~~~

I closed the bug as cannot reproduce, but Jordan Rose suggested that it could be even better if it explicitly mentioned that it was part of an enum. Something like enum element 'a' does not have an associated value.

I’m going to do that. But before I got started on it, I decided to look at enum element. When I’m looking at Swift errors, I usually infer quite a bit based on context. It can be hard to critically examine the error messages while I’m using them. But enum element seemed really strange now that I was thinking critically.

Looking through the other diagnostics, there were a few that mentioned enum element. But it seemed much clearer, at least to me, to say enum case instead.

By looking at the code that emitted these diagnostics, I found that the compiler has both EnumCaseDecls and EnumElementDecls in its AST. That’s because a case can actually name multiple elements:

enum Planet {
    case mercury, venus, earth, mars, jupiter, saturn, uranus, neptune
}

I don’t think I’d ever write an enum like that, so I’d forgotten about it. But it makes sense that the compiler would need different names for these internally. But using internal names from the compiler usually doesn’t make for clear diagnostics.

I searched for swift enum element, to see if anyone else used this term, and the only results were people asking what these error messages meant. That’s a good sign that they could be improved. But I also searched the Swift documentation about enums. The only use of element there is describing the contents of an array. Additionally, the Planet sample I copied above had this description:

Multiple cases can appear on a single line, separated by commas

That seemed like pretty good evidence that enum case would be a better diagnostic:

  1. There’s no evidence that enum element is a widely recognized/used term
  2. The documentation never uses that term
  3. The documentation explicitly calls these cases

So I decided to go ahead and open a PR to do that.

I used ag to search for occurrences of enum element. I found one diagnostic that used it directly, so I fixed that one.

But I also looked through the tests directory and found some other diagnostics that did. After fixing up those test expectations, I went to look for where they were emitted. They hadn’t turned up in my search. Here’s one example:

@objc
enum BadEnum2: Int {
  @objc(X:)   // expected-error{{'@objc' enum element must have a simple name}}{{10-11=}}
  case X
}

I searched for that diagnostic and found that it didn’t use enum element directly:

ERROR(objc_name_req_nullary,none,
      "'@objc' %0 must have a simple name", (DescriptiveDeclKind))

DescriptiveDeclKind here is the type of parameter that’s being passed to the format string. From previous fixes I’ve made, I knew that the EnumElementDecl must be getting passed to this—and something was rendering that as enum element.

So I did the simple thing: I searched the code for ”enum element”. Luckily, that turned up something that looked like what I wanted:

StringRef Decl::getDescriptiveKindName(DescriptiveDeclKind K) {
#define ENTRY(Kind, String) case DescriptiveDeclKind::Kind: return String
  switch (K) {
  … // other cases omitted for brevity
  ENTRY(EnumElement, "enum element");
  }
}

I had a feeling that changing this to ”enum case” would do what I wanted. I changed it, ran utils/build-script --debug --test, and waited.

That worked! I had to fix up a couple of tests, but I was ready to open a PR. You can view it here.

I think improving diagnostics is great way to contribute to the Swift compiler. It’s comparatively quite easy—in some cases it’s literally just editing strings! I also find it compelling because you can (a) eliminate confusion and (b) increase programmer productivity. The latter hopefully means that we can write better software.

If you have to google or ask about an error message, then it can be improved. So anytime you do, consider opening a PR to fix it!

Entry 4: Derive Equatable & Hashable for Uninhabited Types

14 Jul 2018

After attempting a few failed improvements, I needed another win. I looked through a few tickets, but then I remembered a limitation of Swift that has bothered me: Swift won’t synthesize Equatable and Hashable implementations for uninhabited types.

An uninhabited type is one that can’t be initialized. Never, which is included in the standard library, is one such type:

public enum Never {}

A type like this can easily be made Equatable and Hashable:

extension Never: Equatable {
  public static func == (lhs: Never, rhs: Never) -> Bool {
    // This is actually valid Swift. No `case`s are needed because
    // every _possible_ value has been included.
    switch (lhs, rhs) {
    }
  }
}

extension Never: Hashable {
  public var hashValue: Int {
    switch (lhs, rhs) {
    }
  }
}

But Swift won’t do this for you, even though it’ll do it if you include one or more cases:

// Swift complains that you haven't implemented the required methods
enum NoCases: Hashable {
}

// But this is okay
enum OneCase: Hashable {
  case one
}

So I set out to fix this.

The first task was to find the code for synthesized implementations. I don’t remember how (it was a couple weeks ago due to some personal reasons), but I found this code in DerivedConformanceEquatableHashable.cpp:

/// Common preconditions for Equatable and Hashable.
static bool canDeriveConformance(TypeChecker &tc, DeclContext *DC,
                                 NominalTypeDecl *target,
                                 ProtocolDecl *protocol) {
  // The type must be an enum or a struct.
  if (auto enumDecl = dyn_cast<EnumDecl>(target)) {
    // The enum must have cases.
    if (!enumDecl->hasCases())
      return false;

    // The cases must not have associated values, or all associated values must
    // conform to the protocol.
    return allAssociatedValuesConformToProtocol(tc, DC, enumDecl, protocol);
  }

  if (auto structDecl = dyn_cast<StructDecl>(target)) {
    // All stored properties of the struct must conform to the protocol.
    return allStoredPropertiesConformToProtocol(tc, DC, structDecl, protocol);
  }

  return false;
}

This method was returning false for any enums that didn’t have any cases. It couldn’t be as easy as removing that check, could it? 🤔

No, it couldn’t. I got this strange warning after removing that check:

<unknown>:0: warning: will never be executed

But that was definitely the right first step.

Next, I hunted down the source of that warning. It was in the SIL (Swift Intermediate Language) generation, so it didn’t turn up anything useful.

So, instead, I backed up, set a breakpoint in the canDeriveConformance method that I mentioned above, and walked through the code. Just by stepping over, I ended up in resolveWitnessViaDerivation, which had this line:

// Attempt to derive the witness.
auto derived = TC.deriveProtocolRequirement(DC, derivingTypeDecl, requirement);

Stepping into that led me to the code where the Equatable implementation is derived:

ValueDecl *DerivedConformance::deriveEquatable(ValueDecl *requirement) {
  if (checkAndDiagnoseDisallowedContext(requirement))
    return nullptr;

  // Build the necessary decl.
  if (requirement->getBaseName() == "==") {
    if (auto ED = dyn_cast<EnumDecl>(Nominal)) {
      auto bodySynthesizer =
          ED->hasOnlyCasesWithoutAssociatedValues()
              ? &deriveBodyEquatable_enum_noAssociatedValues_eq
              : &deriveBodyEquatable_enum_hasAssociatedValues_eq;
      return deriveEquatable_eq(*this, TC.Context.Id_derived_enum_equals,
                                bodySynthesizer);
    } else if (isa<StructDecl>(Nominal))
      return deriveEquatable_eq(*this, TC.Context.Id_derived_struct_equals,
                                &deriveBodyEquatable_struct_eq);
    else
      llvm_unreachable("todo");
  }
  TC.diagnose(requirement->getLoc(), diag::broken_equatable_requirement);
  return nullptr;

This was what I was looking for. If the requirement is to add ==, then Swift looks to see if it’s an enum or a struct and synthesizes an appropriate implementation. Interestingly, the enum version has a check: the synthesized implementation is different depending on whether the enum has any associated values.

An enum without any cases has only cases without associated values, so that’s the implementation the compiler would be using.

/// Derive the body for an '==' operator for an enum that has no associated
/// values. This generates code that converts each value to its integer ordinal
/// and compares them, which produces an optimal single icmp instruction.
static void
deriveBodyEquatable_enum_noAssociatedValues_eq(AbstractFunctionDecl *eqDecl) {
  auto parentDC = eqDecl->getDeclContext();
  ASTContext &C = parentDC->getASTContext();

  auto args = eqDecl->getParameterLists().back();
  auto aParam = args->get(0);
  auto bParam = args->get(1);

  auto enumDecl = cast<EnumDecl>(aParam->getType()->getAnyNominal());

  // Generate the conversion from the enums to integer indices.
  SmallVector<ASTNode, 6> statements;
  DeclRefExpr *aIndex = convertEnumToIndex(statements, parentDC, enumDecl,
                                           aParam, eqDecl, "index_a");
  DeclRefExpr *bIndex = convertEnumToIndex(statements, parentDC, enumDecl,
                                           bParam, eqDecl, "index_b");

  // Generate the compare of the indices.
  FuncDecl *cmpFunc = C.getEqualIntDecl();
  assert(cmpFunc && "should have a == for int as we already checked for it");

  auto fnType = cmpFunc->getInterfaceType()->castTo<FunctionType>();

  // Leaving out some uninteresting code for brevity
  Expr *cmpFuncExpr = …

  TupleExpr *abTuple = TupleExpr::create(C, SourceLoc(), { aIndex, bIndex },
                                         { }, { }, SourceLoc(),
                                         /*HasTrailingClosure*/ false,
                                         /*Implicit*/ true);

  auto *cmpExpr = new (C) BinaryExpr(cmpFuncExpr, abTuple, /*implicit*/ true);
  statements.push_back(new (C) ReturnStmt(SourceLoc(), cmpExpr));

  BraceStmt *body = BraceStmt::create(C, SourceLoc(), statements, SourceLoc());
  eqDecl->setBody(body);
}

The included documentation comment describes it well. Swift is generating an AST for the code it’s synthesizing. This leads to the error we were seeing:

  1. The compiler detects that some of the generated code will never be executed since the type is uninhabited
  2. The source location is unknown because there isn’t one: it’s synthesized

The other case, where an enum does have associated values, looks similar but actually generates a switch:

// switch (a, b) { <case statements> }
auto aRef = new (C) DeclRefExpr(aParam, DeclNameLoc(), /*implicit*/true);
auto bRef = new (C) DeclRefExpr(bParam, DeclNameLoc(), /*implicit*/true);
auto abExpr = TupleExpr::create(C, SourceLoc(), { aRef, bRef }, {}, {},
                                SourceLoc(), /*HasTrailingClosure*/ false,
                                /*implicit*/ true);
auto switchStmt = SwitchStmt::create(LabeledStmtInfo(), SourceLoc(), abExpr,
                                     SourceLoc(), cases, SourceLoc(), C);
statements.push_back(switchStmt);

auto body = BraceStmt::create(C, SourceLoc(), statements, SourceLoc());
eqDecl->setBody(body);

Using these two versions, I was able to write a new version that works for uninhabited types. It takes advantage of the implementations I gave above: a switch can be empty of the enum has no cases. (I basically copied the associated values variant and neutered it.)

static void
deriveBodyEquatable_enum_uninhabited_eq(AbstractFunctionDecl *eqDecl) {
  auto parentDC = eqDecl->getDeclContext();
  ASTContext &C = parentDC->getASTContext();

  auto args = eqDecl->getParameterLists().back();
  auto aParam = args->get(0);
  auto bParam = args->get(1);

  assert(!cast<EnumDecl>(aParam->getType()->getAnyNominal())->hasCases());

  SmallVector<ASTNode, 1> statements;
  SmallVector<ASTNode, 0> cases;

  // switch (a, b) { }
  auto aRef = new (C) DeclRefExpr(aParam, DeclNameLoc(), /*implicit*/ true);
  auto bRef = new (C) DeclRefExpr(bParam, DeclNameLoc(), /*implicit*/ true);
  auto abExpr = TupleExpr::create(C, SourceLoc(), {aRef, bRef}, {}, {},
                                  SourceLoc(), /*HasTrailingClosure*/ false,
                                  /*implicit*/ true);
  auto switchStmt = SwitchStmt::create(LabeledStmtInfo(), SourceLoc(), abExpr,
                                       SourceLoc(), cases, SourceLoc(), C);
  statements.push_back(switchStmt);

  auto body = BraceStmt::create(C, SourceLoc(), statements, SourceLoc());
  eqDecl->setBody(body);
}

Now I had to tell the compiler to use this variant for enums with no cases. Luckily, I know that the EnumDecl had a hasCases() method from the check I removed above. So I just had to plug that in:

auto bodySynthesizer =
    !ed->hasCases()
        ? &deriveBodyEquatable_enum_uninhabited_eq
        : ed->hasOnlyCasesWithoutAssociatedValues()
              ? &deriveBodyEquatable_enum_noAssociatedValues_eq
              : &deriveBodyEquatable_enum_hasAssociatedValues_eq;
return deriveEquatable_eq(*this, TC.Context.Id_derived_enum_equals,
                          bodySynthesizer);

Compiling and running my test file showed that it worked!

From there, I just had to write a test. Luckily, I found an existing test that tested that an enum with no cases wouldn’t derive conformance. I inverted the expectations from that test and had completed my task!

You can see all of this in my PR.

Entry 3: Unmodified var warnings

27 Jun 2018

I’m a sucker for old bugs. I find it especially satisfying to fix something that’s been broken or missing for some time.

Looking through some older type-checker bugs, I found SR-1658:

func test() {
  // This ought to give a warning to change the var to a let
  var x: Int
  if Int("abc") == nil {
    x = 1
  } else {
    x = 2
  }
  print(x)
}

I was really surprised that this didn’t already give a warning. I guess I’ve just assumed that Swift would warn, so I never paid it too much attention. But I played with it a bit more and found that this didn’t give a warning either:

func test() {
  var x: Int
  x = 1
  print(x)
}

Now I was confused and intrigued.

Trying to understand a bit more

I did my usual dance to find the source of the warning I expected:

  1. Change my test case to get the actual warning
  2. Search the codebase for the text to find the identifier of the diagnostic
  3. Search for the identifier to find where the diagnostic is evoked

That led me to VarDeclUsageChecker. That certainly seemed like the right place.

But as I continued to understand what was going on, I decided to also look at uninitialized errors. I knew that it would trigger in the original example. And since both have to track reads and writes to a var or let, I thought they’d be related.

I was wrong. Uninitialized warnings come from LifetimeChecker in swiftSILOptimizer. This is a completely separate library and phase of compilation.

It still seems like these things ought to be done together.

VarDeclUsageChecker

I thought I’d try to press on and fix my issue anyway. Fixing a minor issue would yield an immediate improvement and help me learn what I needed to make a larger change.

VarDeclUsageChecker is another AST walker. It visits the different nodes of the AST, building up information. Then, in its destructor, it emits various diagnostics.

AST walkers have a few hooks. Besides the per-node visit functions, there’s also “visit declaration before”, “visit declaration after”, “visit expression before”, “visit expression after”, “visit statement before”, and “visit statement after”. These are called, no matter the type of declaration/expression/statement, before/after the specific visit function is called.

Most of the var usage checking happens in these methods. Each declaration node adds the declaration of each variable to a SmallMapVector (a sorted dictionary) of all the variables that are being tracked.

Each declaration is associated with a bit field.

  // Keep track of some information about a variable.
  enum {
    RK_Read        = 1,      ///< Whether it was ever read.
    RK_Written     = 2,      ///< Whether it was ever written or passed inout.

    RK_CaptureList = 4       ///< Var is an entry in a capture list.
  };

Then the expressions are walked. If a variable is referenced, it’s marked as having been read. If it’s assigned to, it’s marked as being written. If it’s used as an inout reference or a closure capture, then it’s marked as read and written (since you don’t know).

Afterwards, you can emit diagnostics:

  • If a var was never read or written, the declaration can be deleted
  • If a var was only written once, it can be a let

A few other caveats also apply. e.g. if you do var (x, y, z) = … and only y is mutated, no warning is given to use let for x and z.

Trying to fix the issue

Having understood the basics of how this worked, I thought I’d be able to improve the diagnostic by adding another flag: RK_Initialized.

func test() {
  var x: Int
  x = 1
  print(x)
}

The x = 1 here is initializing the variable, but it’s counted as a write. By separating writes and initialization, the compiler could detect the above case and still suggest a let.

I added RK_Initialized and looked for the correct place to start using it. I figured my change would have 2 parts:

  1. Start the variable as initialized if it’s declared with a value
  2. Change the first written to initialized if it wasn’t already initialized

I started by trying to set the initialized flag on declaration using this code:

func f() {
  let y: Int = 1
  print(y)
}

A look at the AST showed what I should be looking for:

(pattern_binding_decl
  (pattern_typed type='Int'
    (pattern_named type='Int' 'y')
    (type_ident
      (component id='Int' bind=Swift.(file).Int)))
  (call_expr implicit type='Int' location=1658.swift:6:15 range=[1658.swift:6:15 - line:6:15] nothrow arg_labels=_builtinIntegerLiteral:
    (constructor_ref_call_expr implicit type='(_MaxBuiltinIntegerType) -> Int' location=1658.swift:6:15 range=[1658.swift:6:15 - line:6:15] nothrow
      (declref_expr implicit type='(Int.Type) -> (_MaxBuiltinIntegerType) -> Int' location=1658.swift:6:15 range=[1658.swift:6:15 - line:6:15] decl=Swift.(file).Int.init(_builtinIntegerLiteral:) function_ref=single)
      (type_expr implicit type='Int.Type' location=1658.swift:6:15 range=[1658.swift:6:15 - line:6:15] typerepr='Int'))
    (tuple_expr implicit type='(_builtinIntegerLiteral: Int2048)' location=1658.swift:6:15 range=[1658.swift:6:15 - line:6:15] names=_builtinIntegerLiteral
      (integer_literal_expr type='Int2048' location=1658.swift:6:15 range=[1658.swift:6:15 - line:6:15] value=1))))

(var_decl "y" type='Int' interface type='Int' access=private storage_kind=stored)

Variable declarations happen within a pattern binding, which contains 1 or more assignments.

The code in walkToDeclPre actually noted that these were explicitly ignored:

    // Note that we ignore the initialization behavior of PatternBindingDecls,
    // but we do want to walk into them, because we want to see any uses or
    // other things going on in the initializer expressions.

I deleted the comment and found some other code the looked at PatternBindingDecls. I used what I found there to help me mark initialized variables as initialized:

    // Look for the pattern binding declarations
    if (auto *pbd = dyn_cast<PatternBindingDecl>(D)) {
      // Look at each item in the binding. `let (x, y) = (1, 2), z: Int` has
      // 2 items: `(x, y)` and `z`.
      for (PatternBindingEntry pbe : pbd->getPatternList()) {
        // We only care about bindings that have an init since we're
        // specifically trying to mark them as initialized
        if (!pbe.getInit())
          continue;

        // Mark each variable in the entry (`x` and `y`) as initialized
        pbe.getPattern()->forEachVariable([&](VarDecl *VD) {
          VarDecls[VD] |= RK_Initialized;
        });
      }
    }

Next, I needed to track the first write as an initialization. This was easy to hack together. All the reads and writes funnel through a central method. I modified it to mark the first write as an initialization instead:

  void addMark(Decl *D, unsigned Flag) {
    auto *vd = dyn_cast<VarDecl>(D);
    if (!vd) return;

    auto vdi = VarDecls.find(vd);
    if (vdi != VarDecls.end()) {
      if ((Flag & RK_Written) && !(vdi->second & RK_Initialized))
        vdi->second |= (Flag & ~RK_Written) | RK_Initialized;
      else
        vdi->second |= Flag;
    }
  }

This wasn’t very clean, but I wanted to focus on a proof-of-concept first. I’d clean it up later, if it worked.

I quickly discovered that I’d also need to check !(Flag & RK_Read). When a variable is passed as inout to a function, it’s marked as both read and write—since the compiler doesn’t know what the function will do. In those cases, I needed to preserve the RK_Written mark.

Lastly, I needed to update a few other variable declarations to include the initialized flag. E.g., if you have a top-level variable declaration in a file, that declaration was handled here:

    if (auto *TLCD = dyn_cast<TopLevelCodeDecl>(D)) {
      // If this is a TopLevelCodeDecl, scan for global variables
      auto *body = TLCD->getBody();
      for (auto node : body->getElements()) {
        if (node.is<Decl *>()) {
          // Flag all variables in a PatternBindingDecl
          Decl *D = node.get<Decl *>();
          auto *PBD = dyn_cast<PatternBindingDecl>(D);
          if (!PBD) continue;
          for (PatternBindingEntry PBE : PBD->getPatternList()) {
            PBE.getPattern()->forEachVariable([&](VarDecl *VD) {
              VarDecls[VD] = RK_Initialized|RK_Read|RK_Written;
            });
          }
        } else if (node.is<Stmt *>()) {
          // Flag all variables in guard statements
          Stmt *S = node.get<Stmt *>();
          auto *GS = dyn_cast<GuardStmt>(S);
          if (!GS) continue;
          for (StmtConditionElement SCE : GS->getCond()) {
            if (auto pattern = SCE.getPatternOrNull()) {
              pattern->forEachVariable([&](VarDecl *VD) {
                VarDecls[VD] = RK_Read|RK_Initialized;
              });
            }
          }
        }
      }

After verifying that my test case emitted the warning I wanted, I started working through the test suite. Running it turned up quite a few failures, but many of them were legitimate! That was good news: the warning would be useful.

Unfortunately, I discovered some test cases that I couldn’t handle:

var s : String
for _ in [1, 2] {
    s = " "
}
return s

In this case, the write serves as both a write and as initialization—because it’s inside a loop.

To correctly detect this, I’d need to know if any loops existed between the declaration of the variable and the write. I haven’t figured out a good way to do this yet.

Regrouping

To recap, I thought it’d be easier to warn in this case:

var x: Int
x = 1
print(x)

But it turned out to be almost as hard as the case described in the bug:

var x: Int
if Int("abc") == nil {
  x = 1
} else {
  x = 2
}
print(x)

So I think it’s worth considering how to solve the general case before proceeding. I mentioned above that uninitialized variable errors are handled elsewhere:

var x: Int
if Int("abc") == nil {
} else {
  x = 2
}
print(x) // This correctly errors because x wasn't set

It seems to me like those two bits of code should be combined. Both are tracking reads and writes to variables and emitting diagnostics. But since I’m not certain of this, I’ve asked on the forums. Hopefully that will provide some helpful direction for the future.

Entry 2: SR-7177, Adding a diagnostic

15 Jun 2018

In Entry 0 I pattern matched to add a missing constraint to a KeyPath subscript. In Entry 1 I improved an error message. Now it’s time to put combine my skills to really improve a diagnostic (a term that encompasses both errors and warnings).

This one was a bit of a ride.

Starting point

SR-7177 was filed because of a confusing diagnostic. At some point, that diagnostic improved. But it was still a bit confusing:

var a = [1,2,3,4]
var b = a.popFirst()

error: '[Int]' requires the types '[Int]' and 'ArraySlice<Int>' be equivalent to use 'popFirst'
var b = a.popFirst()
          ^

Uh… okay? Who does [Int] think it is? And why does it require [Int] and ArraySlice<Int> be equivalent? Those will never be equivalent. Where does this popFirst come from?

I was genuinely confused by this one. Looking up the documentation for popFirst() didn’t turn up anything useful, although I did find that it was defined on Collection. (I’m not the only one who can’t keep all the these protocols straight, right?)

But looking up the definition of popFirst() cleared things up:

extension Collection where Self == Self.SubSequence {
    /// Removes and returns the first element of the collection.
    ///
    /// - Returns: The first element of the collection if the collection is
    ///   not empty; otherwise, `nil`.
    ///
    /// - Complexity: O(1)
    public mutating func popFirst() -> Self.Element?
}

You can’t use popFirst on an Array because Array.SubSequence == ArraySlice. It sure would be nice if the diagnostic would say that!

Finding the diagnostic

The first step was to find the existing diagnostic. A quick search for be equivalent to use turned up the definition in DiagnosticsSema.def:

ERROR(types_not_equal_in_call,none,
      "%0 requires the types %1 and %2 be equivalent to use %3",
      (Type, Type, Type, DeclName))

Searching for that id, types_not_equal_in_call, pinpointed where this was called in CSDiag.cpp (the file where diagnostics are created from the constraint solver) inside a function called diagnoseTypeRequirementFailure.

This function is used to diagnose errors related to type requirements (duh!). In other words, errors related to where clauses.

Inside is this code, which chooses a diagnostic based on the type of failed constraint in the clause:

  switch (constraint->getKind()) {
  case ConstraintKind::ConformsTo:
    TC.diagnose(anchor->getLoc(), diag::type_does_not_conform_owner, ownerType,
                lhs, rhs);
    return true;

  case ConstraintKind::Subtype: // superclass
    TC.diagnose(anchor->getLoc(), diag::type_does_not_inherit, ownerType, lhs,
                rhs);
    return true;

  case ConstraintKind::Equal: { // same type
    if (auto *UDE = dyn_cast<UnresolvedDotExpr>(anchor)) {
      TC.diagnose(UDE->getLoc(), diag::types_not_equal_in_call, ownerType, lhs,
                  rhs, UDE->getName());
    } else {
      TC.diagnose(anchor->getLoc(), diag::types_not_equal, ownerType, lhs, rhs);
    }
    return true;
  }

Since this is a == constraint, the last case is used. We can see that it already does a little pattern matching. If it’s an unresolved dot expression—something that uses a method or property or an unknown (hence unresolved) type—then the error includes the name of the method.

That matches what we saw:

error: '[Int]' requires the types '[Int]' and 'ArraySlice<Int>' be equivalent to use 'popFirst'
var b = a.popFirst()
          ^

What would a better error be?

I thought this would ideally match the definition as much as possible. Perhaps:

error: 'Collection' requires the types 'Self' ('[Int]') and 'Self.SubSequence' ('ArraySlice<Int>') be equivalent to use 'popFirst'
var b = a.popFirst()
          ^

And in many places, Swift will give you “note” that tells you where a method was declared. So ideally this would include a note like this too:

Swift.Collection:2:37: note: 'popFirst()' declared here
    @inlinable public mutating func popFirst() -> Self.Element?
                                    ^

Then I wouldn’t have had to hunt around for the definition. I (hopefully) could just click on it in Xcode.

Attempt #1

I started by trying to add the note, since I thought I could figure that out more easily. And since it’s always easiest to copy existing code, I tried to find the source of one of the existing declared here notes—mostly because I couldn’t remember the exact text.

Untitled.swift:1:5: error: value of type 'String' has no member 'coun'; did you mean 'count'?
_ = "".coun()
    ^~ ~~~~
       count
Swift.String:10:16: note: 'count' declared here
    public var count: Int { get }
               ^
Swift.Collection:5:16: note: 'count' declared here
    public var count: Int { get }
               ^

Bingo.

Searching back in DiagnosticsSema.def for declared here turned up a number of results. Most of them either lacked a format parameter or, based on the name, were clearly not what I was looking for. But decl_declared_here looked promising:

NOTE(decl_declared_here,none,
     "%0 declared here", (DeclName))

Searching turned up a lot of usages like this:

tc.diagnose(func, diag::decl_declared_here, func->getFullName());

So what was a DeclName and how could I get one?

Since the types_not_equal_in_call diagnostic that I was trying to improve also passed a DeclName, I tried copying that.

TC.diagnose(UDE->getLoc(), diag::decl_declared_here, UDE->getName());

But that printed a note that pointed to the line that was using popFirst, which was neither true nor helpful. 🙃

note: 'popFirst' declared here
var b = a.popFirst()
          ^

UDE is an UnresolvedDotExpr—a method call where the type wasn’t known. But I knew that the type was known. If it wasn’t, then we wouldn’t get an error about the type requirements.

At first, I thought I might be able to get this information from the type parameter constraint. But dumping the constraint system showed where that information resided:

Score: 0 0 0 0 0 0 0 0 0 0 0
  Fix-it applied, fixed expression was:
Type Variables:
  $T0 [lvalue allowed] as @lvalue [Int] @ locator@0x11d087600 [DeclRef@7177.swift:2:9]
  $T1 [lvalue allowed] as () -> Int? @ locator@0x11d087680 [UnresolvedDot@7177.swift:2:11 -> member]
  $T2 as [Int] @ locator@0x11d0876e0 [UnresolvedDot@7177.swift:2:11 -> member -> archetype 'Self']

Active Constraints:

Inactive Constraints:
Resolved overloads:
  selected overload set choice @lvalue [Int].popFirst: $T1 == () -> $T2.Element?
  selected overload set choice a: $T0 == @lvalue [Int]


Opened types:
    cs.dump()
  locator@0x11d087680 [UnresolvedDot@7177.swift:2:11 -> member] opens τ_0_0 -> $T2

Failed constraint:
  $T2 equal $T2.SubSequence [[locator@0x11d0877f8 [UnresolvedDot@7177.swift:2:11 -> member -> opened generic -> type parameter requirement #1]]];

The unresolved dot expression had a resolved overload in the constraint system. I just needed to figure out how to get it.

I searched in the dump method and found that that list was part of ConstraintSystem::resolvedOverloadSets. Searching for use of that turned up the ConstraintSystem::findResolvedMemberRef method, which seemed to do just what I wanted.

My first attempt to actually use it failed—the resolved member wasn’t found. I figured this was because I was passing in the wrong ConstraintLocator. The ConstraintLocator I was working with looked like this:

(lldb) e locator->dump(&cs)
locator@0x117187bf8 [UnresolvedDot@7177.swift:2:11 -> member -> opened generic -> type parameter requirement #1]

Each locator is made up of an _anchor—the expression it’s from—and a list of PathElements. Those elements show where the constraint came from.

Searching for other uses of findResolvedMemberRef showed that you can ask for a new ConstraintLocator with a specific type of path element:

  auto loc = cs.getConstraintLocator(UDE, ConstraintLocator::Member);
  auto *member = cs.findResolvedMemberRef(loc);

member here is a ValueDecl, which I could pass as the source of the note so that it appeared on the declaration of the method.

Now that I had the note in place, I had to figure out how to and whether I could show the types that I wanted. With my debugger parked on the existing diagnostic, I started exploring the types I had access to.

By jumping through the headers, I discovered that member had access to the name of the type it was defined on. That makes sense: Collection.popFirst should know that it’s defined on Collection.

(lldb) e member->getDeclContext()->getDeclaredInterfaceType()->dump()
(protocol_type decl=Swift.(file).Collection)

Now I could replace the first type in the diagnostic with that.

error: '[Int]' requires the types '[Int]' and 'ArraySlice<Int>' be equivalent to use 'popFirst'
var b = a.popFirst()
          ^

But I still wanted to show that this was from the Self == Self.SubSequence constraint. That constraint was available in the diagnostic method. Dumping the constraint’s types to the console gave me a clue of where to go next:

(lldb) e constraint->getFirstType()->dump()
(type_variable_type id=2)
(lldb) e constraint->getSecondType()->dump()
(dependent_member_type assoc_type=Swift.(file).Sequence.SubSequence
  (base=type_variable_type id=2))

The constraint was of the form $T2 == $T2.SubSequence because the solver was figuring out the concrete Collection type that it should use. I searched for dependent_member_type to see what that was and discovered DependentMemberType (a straightforward translation in retrospect).

DependentMemberType has a base type and either a name or associated type.

/// A type that refers to a member type of some type that is dependent on a
/// generic parameter.
class DependentMemberType : public TypeBase {
  Type Base;
  llvm::PointerUnion<Identifier, AssociatedTypeDecl *> NameOrAssocType;

  …
}

That seemed like what I’d want to include with the diagnostic. But I didn’t want to show $T2.SubSequence, I wanted to show [Int].SubSequence. So I created a new DependentMemberType that used the [Int] type I had with the .SubSequence from the existing DependentMemberType:

auto rhsType = constraint->getSecondType()->getAs<DependentMemberType>();
if (!rhsType)
  return false;
auto memberType = DependentMemberType::get(lhs, rhsType->getAssocType());

Adding a new diagnostic that included the extra type gave me a reasonable diagnostic.

error: 'Collection' requires the types '[Int]' and 'Array<Int>.SubSequence' ('ArraySlice<Int>') be equivalent to use 'popFirst'
var b = a.popFirst()
          ^
Swift.Collection:2:37: note: 'popFirst()' declared here
    @inlinable public mutating func popFirst() -> Self.Element?
                                    ^

I was pretty happy with that, so I opened a PR

Attempt #2

When I opened a PR with the above diagnostic, Pavel Yaskevich pointed out that Swift already had a similar diagnostic that would be clearer:

func countOf<C: Collection>(_ c: C) -> Int where C == C.SubSequence {
    return c.count
}
_ = countOf([1])

error: cannot invoke 'countOf(_:)' with an argument list of type '([Int])'
_ = countOf([1])
            ^
note: candidate requires that the types '[Int]' and 'ArraySlice<Int>' be equivalent (requirement specified as 'C' == 'C.SubSequence' [with C = [Int]])
func countOf<C: Collection>(_ c: C) -> Int where C == C.SubSequence {
     ^

So back I went to the code. I found where the diagnostic was generated and saw that it used a Requirement. That was a new type to me. It’s the AST-equivalent of the Constraint I used above—it represents C == C.SubSequence in the code above. From that Requirement, a Constraint is generated.

Unfortunately, I didn’t see how to work back from the Constraint to the Requirement. But Pavel very helpfully told me that I could get there from the ConstraintLocator.

auto path = locator->getPath();
if (path.empty())
  return false;

auto &last = path.back();
if (last.getKind() != ConstraintLocator::TypeParameterRequirement)
  return false;

auto req = member
  ->getAsGenericContext()
  ->getGenericSignature()
  ->getRequirements()[last.getValue()];

Remember the constraint locator I mentioned above?

(lldb) e locator->dump(&cs)
locator@0x117187bf8 [UnresolvedDot@7177.swift:2:11 -> member -> opened generic -> type parameter requirement #1]

The last item in the path is the type parameter requirement at index 1. By looking up the requirements for the generic signature of the popFirst method, I could use that index to get the requirement I was after.

Then I was able look up the diagnostic that Pavel pointed me to and use its code to create a similar diagnostic note here. And since the note pointed out that the candidate had an un-met requirement, I realized that the more general “has no member” error could be used.

error: value of type '[Int]' has no member 'popFirst'
var b = a.popFirst()
          ^
Swift.Collection:2:37: note: candidate requires that the types '[Int]' and 'ArraySlice<Int>' be equivalent (requirement specified as 'Self' == 'Self.SubSequence')
    @inlinable public mutating func popFirst() -> Self.Element?
                                    ^

I like that even more than my first error. Consistency between error messages is definitely helpful.

This attempt seems much easier—my description of it is at least much shorter—but it was actually more difficult for me to figure out. That’s in part because I used what I learned in the first attempt. But it also makes sense: I had to learn some new things to do things this way.

Detour: why did it spell out Array in my first attempt?

I was surprised to see that the error from my first attempt specified [Int] but Array<Int>.SubSequence instead of [Int].SubSequence, which I would have preferred since the connection is clearer.

error: 'Collection' requires the types '[Int]' and 'Array<Int>.SubSequence' ('ArraySlice<Int>') be equivalent to use 'popFirst'
var b = a.popFirst()
          ^

My first attempt to figure this out was to “Jump to Definition” in Xcode on the diagnose method. That didn’t lead anywhere useful since that method uses template magic.

So I resorted to searching for '.' and setting breakpoints. My search turned up a promising result in DiagnosticEngine.cpp, but the breakpoint wasn’t hit. Since I was pretty sure that DiagnosticEngine::emitDiagnostic was the correct method, I set a breakpoint at the beginning of the method.

Stepping through, nothing exciting happened until the end. I eventually realized that I should step into Consumer->handleDiagnostic since it took a list of arguments. I wasn’t sure whether the arguments would already be stringified at this point, so I kept stepping through.

This eventually lead me to the formatDiagnosticArgument function in DiagnosticEngine.cpp. That function switches on the type of argument. Since I knew I had a Type, I found the relevant case:

  case DiagnosticArgumentKind::Type: {
    assert(Modifier.empty() && "Improper modifier for Type argument");

    // Strip extraneous parentheses; they add no value.
    auto type = Arg.getAsType()->getWithoutParens();
    std::string typeName = type->getString();

    if (shouldShowAKA(type, typeName)) {
      llvm::SmallString<256> AkaText;
      llvm::raw_svector_ostream OutAka(AkaText);
      OutAka << type->getCanonicalType();
      Out << llvm::format(FormatOpts.AKAFormatString.c_str(), typeName.c_str(),
                          AkaText.c_str());
    } else {
      Out << FormatOpts.OpeningQuotationMark << typeName
          << FormatOpts.ClosingQuotationMark;
    }
    break;
  }

type->getString() stood out. Jumping to its definition wasn’t very enlightening:

std::string TypeBase::getString(const PrintOptions &PO) const {
  std::string Result;
  llvm::raw_string_ostream OS(Result);
  print(OS, PO);
  return OS.str();
}

But jumping through a couple print functions like this eventually led me to this function:

void Type::print(ASTPrinter &Printer, const PrintOptions &PO) const {
  if (isNull())
    Printer << "<null>";
  else
    TypePrinter(Printer, PO).visit(*this);
}

TypePrinter definitely looked like it was what I needed. It had a series of visit* methods for different types. I knew I had a DependentMemberType, since I’d constructed it above, so I looked for that method.

  void visitDependentMemberType(DependentMemberType *T) {
    visitParentType(T->getBase());
    Printer << ".";
    Printer.printName(T->getName());
  }

The parent type was clearly what I was after.

  void visitParentType(Type T) {
    PrintOptions innerOptions = Options;
    innerOptions.SynthesizeSugarOnTypes = false;

    if (auto sugarType = dyn_cast<SyntaxSugarType>(T.getPointer()))
      T = sugarType->getImplementationType();

    TypePrinter(Printer, innerOptions).visit(T);
  }

SynthesizeSugarOnTypes definitely seemed like it would control this. [Int] is syntactic sugar for Array<Int>. Just to be sure, I set a debugger breakpoint after that line and ran e innerOptions.SynthesizeSugarOnTypes = true in the debugger. After continuing, I saw [Int].SubSequence just as I was hoping.

But since this was explicitly turned off, I decided to leave it for now. It seemed like a much larger and potentially unwanted change. So this ended up being an insightful, but otherwise unproductive, detour.

Finishing up

Since I wasn’t sure I could correctly find all the test cases that I’d need to change up front, I decided to let the computer do this for me. I ran ./utils/build-script --debug --test while I wrote most of this up. 😄

After the tests finished, I looked at all the failed tests. Once I’d made sure that the failure actually had the error that I’d want, I updated the test expectation to match.

You can see my PR here.

Entry 1: A better “no subscripts” error

14 Jun 2018

While working on SR-7380, I came across this error in Swift 4.1:

error: type 'Int' has no subscript members
_ = 1[foo: 2]
    ^

It makes sense now, but I was a little confused at the time. (And it was more confusing since it was in a case where it should have been valid except for the bug I was trying to fix.)

But this seemed like something I could improve, so I thought I’d try. (And improving diagnostics is one of the reasons why I’m interested in the type checker.)

Where does this error come from?

I actually like debugging from diagnostics (a term that includes errors, warnings, and “notes”) because they’re easy to pin down. In this case, I just had to search for “has no subscript members”.

(As an aside: I finding searching to be one of the best ways to navigate a codebase. I use Xcode’s “Find in Project” and a tool like the Silver Searcher constantly. A search tool and some basic regexes can get you really far.)

Searching for that error turns up a rather large file, DiagnosticsSema.def. This file is full of definitions like this:

ERROR(type_not_subscriptable,none,
      "type %0 has no subscript members"
      (Type))

This defines the diagnostic. This one is an error. It’s referred to from the code as type_not_subscriptable. And it has a format string that defines the error and takes a Type as its only argument.

Simple enough.

What should the error be?

Deciding what to replace it with was actually the hardest part.

Looking around the definitions file a bit, I eventually noticed errors like this:

error: value of type 'Int' has no member 'foo'
_ = 1.foo()
    ^ ~~~

I don’t think I’ve ever been confused by this error, so I decided to try to get closer to it.

First, I noticed that it said value of type instead of type. That seemed to make things a lot clearer.

Then I thought that subscript members seemed a little weird. I searched the Swift subscript documentation and never saw anything refer to them as members. (Although I’ve seen that in the code of the compiler.) Member, I think, is generally used in Swift because it encompasses both methods and properties. (And you can’t tell whether foo.bar() is a method or a property from looking at its usage.)

Instead, most of the diagnostics and documentation referred to them as just subscripts. That’s what I’d call them too, so that seemed like a better description.

error: value of type 'Int' has no subscripts
_ = 1[foo: 2]
    ^

That seemed hugely better to me.

Finishing Up

After getting the description in order, I noticed that the name was also a little different than the names of the similar “has no member” errors. So I renamed type_not_subscriptable to could_not_find_value_subscript to match. This was an easy search-and-replace.

The last step was to update the tests. The generated Xcode project doesn’t include the tests files, so I searched from the command line using the Silver Searcher.

Diagnostic tests look something like this:

func almostSubscriptableValueMismatch(_ as1: AlmostSubscriptable, a: A) {
  as1[a] // expected-error{{type 'AlmostSubscriptable' has no subscript members}}
}

A comment describes what the expected error or warning is. Updating them is straightforward: you just change the text.

Once I had that, I ran the tests locally to make sure I didn’t miss anything. You can see my PR here.

Updating an existing diagnostic like this is surprisingly easy. Hopefully you’ll keep that in mind next time you’re confused by a Swift error. This is a great way to start contributing to Swift and an easy way that the community could greatly improve the daily lives of Swift developers. (Although it’s often the case that a confusing error is just a bad diagnostic, which is different altogether.)

Next, I hope to add more and better detail to a diagnostic. That’s a bit more involved than this, but combines what I learned here and in Entry 0.

Entry 0: SR-7380, Ambiguous KeyPath

13 Jun 2018

My first bug fix! Yay!

Here’s the bug I fixed (with a lot of help):

7380.swift:1:16: error: type of expression is ambiguous without more context
"str"[keyPath: \.count]
               ^~~~~~~

That seems obviously broken. The value is a String literal. They KeyPath should obviously be String.count. So why doesn’t this work?

How do I debug this thing?

My first step was to figure out exactly how to debug the issue.

I started by reproducing the issue in the Xcode debugger. Since I’m so familiar with Xcode, this is where I wanted to start. Luckily, Swift supports building with Xcode: you just need to pass --xcode to the build script.

Having done that, I:

  1. Opened the generated Xcode project file from inside the build directory
  2. Manually created a scheme for the swift product
  3. Edited the scheme to:
    1. Change the Run > Options > Working Directory to the directory where I’m keeping my buggy .swift files.
    2. Edit the Run > Arguments to pass the file I’m trying to compile. I like to keep things simple by reusing the bug number. So I’m working with 7380.swift here.
  4. Ran the code and saw the output in the debugger.

Now I could set breakpoints in Xcode and hit them in LLDB.

I had to poke around a bit to set my first breakpoint. I knew that I'd likely be dealing with the type checker, and I knew that type checking was done in swiftSema, the library used for Swift’s semantic analysis.

I eventually stumbled on ConstraintSystem.solve, which seemed like a good place to start.

But I also had to figure out how to print anything meaningful to the debug console. I’ve been pretty spoiled in Objective-C and Swift, where you can override description and use po in the debugger to print out helpful descriptions of your objects.

In C++, I knew that you can use p to print out the address of an object and p *object to print out its memory contents, but that’s not a super helpful description.

While visiting a WWDC lab, I discovered that, within the constraint system at least, most objects have a dump() method you can call e object.dump() to print out a friendly description. Now I could debug.

Extra Logging

Swift includes documentation about debugging the compiler. It includes some helpful flags you can pass (inside the scheme’s Run > Arguments or on the command-line) to get some helpful diagnostics.

Since I knew that I’d be debugging the type-checker, I added -Xfrontend -debug-constraints. (swiftc is the actual compiler, which takes the -debug-constraints flag; -Xfrontend tells swift to pass the next flag through to swiftc.)

That yields a lot of helpful output that shows the type-checker’s search for a solution. But—as I found out later—it doesn’t show you everything. Stepping through with the debugger and dumping this is still very helpful.

Dumping the AST (Abstract Syntax Tree) is also very helpful.

What is type checking?

When you write a Swift program, the compiler puts it through a number of stages that transform some input into some output.

To start with, you have just a file. That’s what we typically work with day-to-day. It’s effectively a big string. The lexer lexes the input stream into a stream of tokens: it transforms if foo { blah() } into something like [Token](.if, .identifier(“foo”), .openBrace, .identifier(“blah”), .leftParen, .rightParen, .closeBrace).

The parser transforms a stream of tokens into an abstract syntax tree (AST). This adds semantics to the tokens, creating something with meaning. That gives something like [Expression](.if(.variable(“foo”), call("blah", []), nil).

But not all valid syntactically-valid Swift programs are semantically valid. e.g. ””[keyPath: \Int.description] is valid syntax, but has invalid semantics. In this example, the types don’t line up: "" is a String, but the KeyPath is on Int.

The type-checker (part of the Sema or Semantic Analysis library in Swift) takes an AST and (1) infers missing types and (2) validates all the types.

How does type checking work?

Type-checking uses a constraint solver to solve a system of unknowns. I’ve written about how constraint solvers work generally and open sourced Logician, a Swift constraint solver library. Those are probably worth checking out if this sounds interesting.

In case of the Swift type-checker, a number of type variables are created for unknown types. Then constraints are added between the types and type variables. Best guesses are made. Swift takes valid solutions, scores them, and chooses one. There’s a lot more detail in the Swift documentation.

Code-wise, type-checking is done expression-by-expression. The constraint system walks the AST with an ASTWalker, generating constraints for each node in the AST of that expression. Once the constraints are generated, the problem can be solved.

Solving works by simplifying constraints and trying out different possible values. If a string literal is used without an explicit type, e.g., then it’ll be constrained to types that are ExpressibleByStringLiteral. The type-checker might start by checking whether all the constraints are met with an actual String.

Once the expression has been type-checked, the AST is rewritten to add the type annotations.

The interesting bits are in a few files:

  • CSGen.cpp: Generates the constraints from the AST
  • CSSolver.cpp: Does the actual solving
  • CSSimplify.cpp: Breaks down constraints once more information is known
  • CSDiag.cpp: Diagnoses type-checking failure, producing warnings and errors
  • CSApply.cpp: Rewrites the AST by applying the solution
  • ConstraintSystem.cpp: The core object that tracks constraints and assignments

If you dump the ConstraintSystem, you’ll see something like this:

Score: 0 0 0 0 0 0 0 0 0 0 0
Type Variables:
  $T0 as String @ locator@0x119800400 [StringLiteral@7380.swift:1:1]
  $T1 subtype_of_existential bindings={} @ locator@0x119800490 [KeyPath@7380.swift:1:16]
  $T2 [lvalue allowed] fully_bound subtype_of_existential involves_type_vars bindings={} @ locator@0x119800490 [KeyPath@7380.swift:1:16]
  $T3 subtype_of_existential involves_type_vars bindings={} @ locator@0x119800490 [KeyPath@7380.swift:1:16]
  $T4 subtype_of_existential involves_type_vars bindings={} @ locator@0x119800490 [KeyPath@7380.swift:1:16]
  $T5 subtype_of_existential involves_type_vars bindings={} @ locator@0x1198006c0 [Subscript@7380.swift:1:6 -> subscript index]
  $T6 [lvalue allowed] subtype_of_existential bindings={} @ locator@0x1198006e0 [Subscript@7380.swift:1:6 -> subscript result]
  $T7 subtype_of_existential bindings={} @ locator@0x119800cf8 [Subscript@7380.swift:1:6 -> subscript member -> function argument]
  $T8 [lvalue allowed] subtype_of_existential involves_type_vars bindings={} @ locator@0x119800cf8 [Subscript@7380.swift:1:6 -> subscript member -> function argument]
  $T9 subtype_of_existential involves_type_vars bindings={} @ locator@0x119800cf8 [Subscript@7380.swift:1:6 -> subscript member -> function argument]

Active Constraints:

Inactive Constraints:
  @lvalue $T1[.count: value] == $T2 [[locator@0x119800510 [KeyPath@7380.swift:1:16 -> key path component #0]]];
  $T2 equal $T3 [[locator@0x119800490 [KeyPath@7380.swift:1:16]]];
  $T4 key path from $T1 -> $T3 [[locator@0x119800490 [KeyPath@7380.swift:1:16]]];
  (keyPath: $T4) arg tuple conv $T5 [[locator@0x1198006c0 [Subscript@7380.swift:1:6 -> subscript index]]];
  $T8 equal $T9 [[locator@0x119800750 [Subscript@7380.swift:1:6 -> subscript member]]];

Examining this is the best way to debug type checking.

What caused the bug?

Back to the bug I wanted to solve:

7380.swift:1:16: error: type of expression is ambiguous without more context
"str"[keyPath: \.count]
               ^~~~~~~

Since this is clearly not ambiguous, we must know something that the type-checker doesn’t.

The output of -debug-constraints starts with a dump of the AST:

(subscript_expr type='$T6' location=7380.swift:1:6 range=[7380.swift:1:1 - line:1:23] arg_labels=keyPath:
  (string_literal_expr type='$T0' location=7380.swift:1:1 range=[7380.swift:1:1 - line:1:1] encoding=utf8 value="str" builtin_initializer=**NULL** initializer=**NULL**)
  (tuple_expr type='(keyPath: $T4)' location=7380.swift:1:6 range=[7380.swift:1:6 - line:1:23] names=keyPath
    (keypath_expr type='$T4' location=7380.swift:1:16 range=[7380.swift:1:16 - line:1:18]
      (component=unresolved_property count type=<null>)
      <<null>>
      (unresolved_dot_expr type='<null>' field 'count' function_ref=unapplied
        (key_path_dot_expr implicit type='<null>')))))

There’s a subscript on a string literal that has an arguments tuple with a keyPath: label and a KeyPath expression inside with an unresolved dot expression. (Unresolved meaning that we don’t know what type it’s on.)

Next, -debug-constraints prints the initial ConstraintSystem:

Score: 0 0 0 0 0 0 0 0 0 0 0
Type Variables:
  $T0 literal=3 bindings={(subtypes of) (default from ExpressibleByStringLiteral) String} @ locator@0x7fa6da01aa00 [StringLiteral@7380.swift:1:1]
  $T1 subtype_of_existential bindings={} @ locator@0x7fa6da01aa90 [KeyPath@7380.swift:1:16]
  $T2 [lvalue allowed] fully_bound subtype_of_existential involves_type_vars bindings={} @ locator@0x7fa6da01aa90 [KeyPath@7380.swift:1:16]
  $T3 subtype_of_existential involves_type_vars bindings={} @ locator@0x7fa6da01aa90 [KeyPath@7380.swift:1:16]
  $T4 subtype_of_existential involves_type_vars bindings={} @ locator@0x7fa6da01aa90 [KeyPath@7380.swift:1:16]
  $T5 fully_bound subtype_of_existential involves_type_vars bindings={} @ locator@0x7fa6da01acc0 [Subscript@7380.swift:1:6 -> subscript index]
  $T6 [lvalue allowed] fully_bound subtype_of_existential involves_type_vars bindings={} @ locator@0x7fa6da01ace0 [Subscript@7380.swift:1:6 -> subscript result]

Active Constraints:

Inactive Constraints:
  $T0 literal conforms to ExpressibleByStringLiteral [[locator@0x7fa6da01aa00 [StringLiteral@7380.swift:1:1]]];
  @lvalue $T1[.count: value] == $T2 [[locator@0x7fa6da01ab10 [KeyPath@7380.swift:1:16 -> key path component #0]]];
  $T2 equal $T3 [[locator@0x7fa6da01aa90 [KeyPath@7380.swift:1:16]]];
  $T4 key path from $T1 -> $T3 [[locator@0x7fa6da01aa90 [KeyPath@7380.swift:1:16]]];
  $T0[.subscript: value] == ($T5) -> $T6 [[locator@0x7fa6da01ad50 [Subscript@7380.swift:1:6 -> subscript member]]];
  (keyPath: $T4) arg tuple conv $T5 [[locator@0x7fa6da01acc0 [Subscript@7380.swift:1:6 -> subscript index]]];

There are a bunch of type variables (unknown types). It’s worth trying to work through them on your own.

From here, we can see why Swift thinks the type of the KeyPath is ambiguous: $T0 (the type of the string literal) has no real connection to $T1 (the root type of the KeyPath). But note that this is just the initial constraints—additional constraints could be provided later that provide this connection.

At this point, Swift hasn’t determined that the subscript is a key path application—it could be any subscript. Subscripts can be overloaded, so an OverloadChoice will be created to try the different choices. This is a disjunction—a spot where Swift can try different solutions.

The rest of -debug-constraints shows Swift trying a few different subscript overloads and then trying to diagnose the type failure. You can set a breakpoint inside the overload choice, step through, and dump the ConstraintSystem to see its logic.

What was the fix?

Well I had quite a bit of help with this in WWDC labs at and try! Swift San Jose. 😅

But it eventually became clear that an additional constraint was needed to tie the type the subscript was operating on to the root type of the KeyPath. But (1) where should that happen and (2) how do you do it?

The right place seems to be inside ConstraintSystem::addKeyPathApplicationConstraint. This is called from within the subscript’s OverloadChoice, when the type checker is trying to solve it as a KeyPath application. That’s the outermost code (excluding the code inside the overload choice) that knows that we’re trying to use a KeyPath application.

That method is called with a ConstraintLocatorBuilder, which can build a ConstraintLocator. A ConstraintLocator locates the origin of a constraint within an expression. The builder, well, can build a locator. In this case, we didn’t need to build an actual locator, we just need to ask it for its parts.

That yields the origin of the key path application constraint, which in this case will be a subscript expression. By looking inside there, and conditionally casting the expressions that we find, we can look for nodes that match the AST we’re interested in.

If this is the case we’re interested in, we can use the KeyPath expression (representing \.count) to find the KeyPath constraint. Once we have that, it’s easy to add a new constraint between the type the subscript operates on and the root type of the KeyPath. With that additional information, Swift can resolve \.count correctly! Bug fixed!