Qualified Lookup in SwiftLexicalLookup

Hi everyone! I’m Filip Sakellariou and I’ll be working on implementing a qualified-lookup query API to SwiftSyntax’s SwiftLexicalLookup module along with my mentor Pavel Yaskevich (@xedin) over the summer as part of GSoC 2026. In this post, I want to understand use cases community members might have for this feature, and propose a preliminary API.

Introduction

As part of his 2024 GSoC project, Jakub implemented the SwiftLexicalLookup module that provides unqualified-lookup capabilities. Unqualified lookup is the process of determining the declaration to which a given identifier node refers. For instance, the following lookup for b:

struct MyStruct {
  func myfunc(a: Int, b: Int?) {
    if let b { 
      print(b) // <-- Lookup `b` here
    }
  }
}

would find the if let b declaration, the b: Int? function argument, and request further lookup in MyStruct.

Qualified lookup looks into a nominal type declaration and finds relevant declarations, providing another powerful AST facility that will allow build tools to build even more powerful macros and libraries. For instance, suppose we look up count declaration inside the Sub class:

class Base {  
  var count: Int { 0 } // <- Result 2️⃣
}
class Sub: Base {
  override var count: Int { 1 } // <- Result 1️⃣
}

Then, we’d return the declarations :one: in the Sub class itself, and :two: in the superclass.

Motivation

Currently, build tools’ main way of performing qualified lookup is through SourceKitten. However, SourceKitten has to dynamically link the compiler backend’s SourceKit library, causing a multitude of hard-to-debug linker errors, especially on non-MacOS platforms like Linux. Apart from distribution challenges, SourceKit’s API is fundamentally designed to service Integrated Development Environments, and doesn’t naturally translate to general build-tool queries.

There’s already evidence that existing SourceKitten clients want to take advantage of SwiftSyntax’s more advanced AST facilities. For example, SwiftLint now utilizes SwiftLexicalScopes to perform unqualified lookup for some of its linting rules. Qualified lookup could offer more precise diagnostics and enable more linting rules (as I outline in the proposal). SwiftJava also uses unqualified lookup and could benefit from the proposed qualified lookup functionality.

Qualified lookup will provide a pure-Swift, reliable solution for build tools to reason about a type’s members, enabling more powerful build tools.

Detailed Design

Based on Doug, Jakub and Pavel’s comments on my initial GSoC proposal, I want to share an early draft of what the API might look like:

extension DeclGroupSyntax {
  /// Search for members matching given identifier.
  /// Parameters:
  /// - lookUpPosition: Indicates position where we should
  ///     start lookup. Will enable future expansion that filters
  ///     by access control.
  func lookupMember(
    _ identifier: Identifier?,
    from lookUpPosition: AbsolutePosition?,
    with config: QualifiedLookupConfig = QualifiedLookupConfig()
  ) -> [QualifiedLookupResult]

In the method above, DeclGroupSyntax basically refers to any nominal type declaration (structs, enums, protocols, classes, actors) and extensions. Then, we specify the identifier we want to look up within this group syntax, along with the position where lookup should commence and additional configuration options. For now, configuration options include an optional ConfiguredRegions type for handling things like #if. Given this information, the qualified-lookup facility returns possible results:

enum QualifiedLookupResult {
  /// Explicitly declared members.
  case members([DeclSyntax], introducedIn: DeclSyntax)
  /// Members declared in conditional extensions.
  case conditionalMembers(
         [DeclSyntax], introducedIn: DeclSyntax, 
         inheritanceClause: InheritanceClauseSyntax?, 
         genericClause: GenericWhereClauseSyntax?)
  /// Implicit members in the given group declaration.
  case implicitMembers([DeclSyntax], introducedIn: DeclGroupSyntaxType)
  /// Need further lookup for `@dynamicMemberLookup`-annotated type
  case lookForDynamicMembers(
         dynamicMemberSubscripts: [SubscriptDeclSyntax], 
         introducedIn: DeclGroupSyntaxType)
  /// Need further lookup for macro attributes.
  case lookForMacros(
         potentialMacroDecl: [DeclSyntax], 
         introducedIn: DeclGroupSyntaxType)
  /// Look for any types we encountered in the lookup that weren't in 
  /// the SymbolTable syntax trees (e.g. the looked up type conforms to
  /// a protocol from a different module).
  case lookForSupertypes(
         inheritedFrom: InheritanceClauseSyntax, 
         genericClause: GenericWhereClauseSyntax?)
}

More specifically, here’s what each result type means

  1. Members

    These are explicitly declared members, including: static/class/instance stored and computed properties, functions, subscripts and initializers, dynamic-member-lookup results, along with nested types, type aliases and associated types. Consider the following struct, for instance:

    struct MyStruct<T> {
      func callAsFunction() {} 
    }
    

    Qualified lookup within the MyStruct scope would return the callAsFunction() function declaration. Note that qualified lookup won't surface operator functions, objc functions using dynamic lookup, and generic parameters like MyStruct.T (semantically wrong).

  2. Conditional members

    These are members declared in conditional extensions, such as:

    extension Array where Element == Int {
      func sum() -> Int { reduce(0, +) } 
    }
    

    Here, qualified lookup will return the sum() declaration in the extension above together with the where Element == Int clause.

  3. Implicit members

    Members the user didn’t explicitly write, including self, Type and synthesized initializers.

  4. Prompt dynamic-member lookup

    Types and protocols annotated with @dynamicMemberLookup have one or more subscripts with a dynamicMember string or keypath argument. We defer to the type-checker to determine what members these subscripts can produce.

  5. Prompt macro-attribute lookup

    Any unknown attributes that could be attached macros or property wrappers that expand to more declarations. For instance:

    struct MyView {
      @State var myState = 0
    }
    

    instructs the tooling to look up what the @State attribute expands to in the variable declaration above (if anything).

  6. Prompt supertype lookup

    Types can conform to protocols, classes may have superclasses and protocols may refine other protocols; these are all examples of "supertypes" on which we have to perform lookup. For example, if our group declaration is a struct conforming to some protocol, we might also need to look up declarations on that protocol.

Although this API is somewhat useful on its own, it can only access one group declaration, making it quite limited. So we can introduce a symbol table that tracks and caches lookup results for an entire source file:

struct SymbolTable {
  /// Construct a table for caching symbol lookup
  /// for the given file syntax.
  init(fileSyntax: SourceFileSyntax)
}

extension DeclGroupSyntax {
  /// Same as `lookupMember` above but can visit supertypes in
  /// the symbol table's `fileSyntax`.
  func lookupMember(
    _ identifier: Identifier?,
    from lookUpPosition: AbsolutePosition?,
    using symbolTable: inout SymbolTable,
    with config: QualifiedTableLookupConfig = QualifiedTableLookupConfig()
  ) -> [QualifiedLookupResult]
}

struct QualifiedTableLookupConfig {
  /// Parameters:
  /// - lookupSuperprotocols: Whether to recursively look up 
  ///       inherited or conformed-to protocols.
  /// - lookupSuperclasses: Whether to recursively look up 
  ///       superclasses, and the protocols they conform to (if
  ///       `lookupSuperprotocols` is true).
  init(
    lookupSuperprotocols: Bool = true, 
    lookupSuperclasses: Bool = true, 
    configuredRegions: ConfiguredRegions? = nil)
}

As you can see, the symbol table allows the qualified-lookup facility to directly perform some supertype lookup. Here's an example:

// File.swift

protocol MyProto {
  func myfunc()
}

extension MyProto {
  func myfunc() {}
}

struct MyStruct: MyProto {}

Suppose we initialize SymbolTree with this entire file and look up myfunc in MyStruct. Qualified lookup can look into the MyProto "supertype" and directly find myfunc, instead of just prompting us to look into the MyProto protocol ourselves. Of course, we can configure whether this supertype lookup should occur for protocols, classes, both or neither.

Since this API works directly on syntax trees, it notably lacks important compiler functionality. We not attempt to perform overload resolution, constraint solving, operator lookup, or AnyObject dynamic lookup. These are left for the client to handle, as with unqualified lookup.

Feedback & Other Use Cases

Does the preliminary API look feasible or did I miss something? Please also let me know if you have a cool use case for qualified lookup so we can make sure that the new API handles it.

Thank you all for your feedback during the GSoC application phase and for taking the time to read this proposal.

Relevant Resources

  1. GSoC 2024 Unqualified Lookup Showcase
  2. GSoC 2026 Project List
  3. Proposal Thread
  4. Proposal Document
11 Likes

This looks great. Excited to see this coming. I’m sure we’ll find good use for refactoring actions in SourceKit-LSP as well.

I have a few thoughts / questions:

  • Do you have an example of how you expect this to be used in conjunction with the existing lexical lookup queries?
  • LookupResult.lookForMembers currently has a Syntax as associated value while the lookup queries you propose run on DeclGroupSyntax. I think it would be good to align these so callers don’t need to do type conversions.
  • lookupMember takes an AbsolutePosition as parameter. I might be missing something here but couldn’t the lookup have been triggered in a different file? In that case, what would be the value of the look up position?
  • I assume that SymbolTable is intended to be shared between multiple calls (indicated by you passing it as inout) and I wonder if it would be nicer to make it a final class instead of a struct, so that you also don’t need to pass it as inout anymore, improving the API ergonomics a bit.
  • Regarding the super type lookup, I think the behavior might be surprising to users if it happens to work within one file (which is likely an easy test case that users will try) and then stops working in the multi-file setups. I wonder if it would be more consistent if we didn’t provide such transitive lookup logic that we can’t offer between different files.
1 Like

Thanks for the feedback! I'll reply in order:

  • Do you have an example of how you expect this to be used in conjunction with the existing lexical lookup queries?

    Yes, we can imagine a scenario where --given an identifier token-- we want to find all the declarations to which it may refer. Unqualified lookup would limit us to the current scope, whereas qualified lookup allows us to access other declarations in the symbol table.

    func findReferencedDecl(
      symbolTable: inout SymbolTable, identifierToken token: TokenSyntax
    ) -> [DeclSyntax] {
      // Ensure token is an identifier
      guard let tokenIdentifier = token.identifier else { return [] }
    
      // Perform qualified lookup on all `lookForMembers` result
      return token.lookup(tokenIdentifier).flatMap { (unqualifiedResult: LookupResult) in
        // Only look at .lookForMembers
        guard case .lookForMembers(let declScope) = unqualifiedResult else {
          continue
        }
    
        // Currently we have to do:
        // guard let declScope = declScope.asProtocol(DeclGroupSyntax.self) else {
        //     fatalError("Expected to look up extension")
        // }
    
        let qualifiedResults = declScope.lookupMember(
          tokenIdentifier, from: token.position, using: &symbolTable)
    
        // Extract the `members` from the qualified lookup.
        return qualifiedResults.flatMap { (qualifiedResult: QualifiedLookupResult) in
          switch qualifiedResult {
          case .members(let members, _): members
          default: []
          }
        }
      }
    }
    

    Note that I assume that lookForMembers returns any DeclGroupSyntax; I discuss the motivation for this below. Also, here's an example where this method would surface more declarations that unqualified lookup alone:

    struct Container {
      var count: Int { ... }
    }
    
    extension Hi {
      func ensureNonEmpty() {
          precondition(count != 0) // <- Look up `count` here
      }
    }
    
    
  • LookupResult.lookForMembers currently has a Syntax as associated value while the lookup queries you propose run on DeclGroupSyntax. I think it would be good to align these so callers don’t need to do type conversions.

    I think the issue is that LookInMembersScopeSyntax doesn't inherit from DeclGroupSyntax and ends up reimplementing much of its functionality. If LookInMembersScopeSyntax inherited from DeclGroupSyntax, then so would NominalTypeDeclSyntax, which would eliminate its genericWhereClause and inheritanceClause requirements. Moreover, internally, we already pass LookInMembersScopeSyntax to .lookForMembers(in:) so this change would merely make the result more explicit.

    The benefit of using DeclGroupSyntax instead of Syntax is that it leaves less room for error. If we use Syntax, users might try to perform qualified lookup on an if expression, which wouldn't make much sense. However, if we change the current .lookForMembers(in:) to return any DeclGroupSyntax (or a struct wrapping said existential), we'd retain the less error-prone API without needing type conversions.

  • lookupMember takes an AbsolutePosition as parameter. I might be missing something here but couldn’t the lookup have been triggered in a different file? In that case, what would be the value of the look up position?

    I was originally thinking of this as an API that operates on a single SourceFileSyntax, so I hadn't considered this case. Would it make sense to document that lookUpPosition needs to be in the same file or nil for queries originating from different files.

    We could also decide to extend SymbolTable to accept multiple source files. Then, rather than passing in a lookUpPosition, we could provide an optional lookupOrigin which would essentially be a SourceFileSyntax-and-AbsolutePosition pair.

    (Also, should we change lookUpPosition to lookupPosition, since we treat "lookup" as a compound noun elsewhere? E.g. the library name is SwiftLexicalLookup)

  • I assume that SymbolTable is intended to be shared between multiple calls (indicated by you passing it as inout) and I wonder if it would be nicer to make it a final class instead of a struct, so that you also don’t need to pass it as inout anymore, improving the API ergonomics a bit.

    Sure. Thinking out loud here, would it perhaps make sense to make it a non-Copyable type to avoid ARC overhead? I don't know if it's worth sacrificing the ergonomics, though.

  • Regarding the super type lookup, I think the behavior might be surprising to users if it happens to work within one file (which is likely an easy test case that users will try) and then stops working in the multi-file setups. I wonder if it would be more consistent if we didn’t provide such transitive lookup logic that we can’t offer between different files.

    Good point. I'll change the default to be no supertype lookup.

That makes sense to me. Looking forward to seeing this play out in practice.

I don’t have any concrete ideas here but I feel like any reasonable setup will need to do multi-file lookup (if only because extensions may be defined in other files) and we should design for that. Whether we support it from day 1 is a separate question but multi-file handling shouldn’t be an afterthought.

That’s something worth exploring but I wouldn’t make any such optimization without first checking whether the ARC overhead has a measurable impact.

2 Likes

Congrats on getting the project! Happy to see more work happening in SwiftLexicalLookup. I like the shape of the API already, I also have two suggestions:

  • In case of the lookForSupertypes flag, I think it would be a good idea to just store it with DeclGroupSyntaxType and let the clients figure out how and what supertypes they should look into. conditionalMembers could also be simplified in a similar way. Specifically, I think the current formulation is not expressive enough to cover the actor edge case where we have an implicit Actor conformance, but syntactically there's no InheritanceClauseSyntax we could refer to. I think it would also be more robust in case any new keywords/intricate semantics like this make its way into the language in the future.
  • Also wanted to ask what does the [DeclSyntax] in implicitMembers refer to? As far as I understand, in case of implicit members, we don't have any concrete syntax nodes to distinguish between them with, so shouldn't we extend and use ImplicitDecl instead? Going further, what would you say about adopting LookupName as the name representation in the result data structure instead of directly using DeclSyntax? That would make it more consistent with unqualified lookup API and you could also remove the distinction between implicit and explicit members inside QualifiedLookupResult.
1 Like

Thanks for the suggestions! I was actually playing around with some more changes to the API so I apologize for the delayed response.

So, if I understand your suggestion correctly, DeclGroupSyntaxType would store the lookForSupertypes property and some sort of conditionalClause property that contains the inheritanceClause and genericClause nodes? As for implicit conformances, I think you make a good point, and IMO we should be more specific and return an enum of the form:

enum Supertype {
  case explicit(TypeSyntax)
  // E.g. enum A { case a } implicitly conforms to Hashable
  case implicitHashable 
  // E.g. actor A {} implicitly conforms to actors
  case implicitActor
}

Yeah, my initial version with [DeclSyntax] wouldn't work. My reservation about using LookupName and/or ImplicitDecl is that qualified lookup never deals with an implicit error, newValue or oldValue; it deals with declarations. Also, it's useful for lookup requests to differentiate between the keyword "self" and the identifier "`self`" both when requesting a name and when receiving lookup results. So I was considering creating separate name types for qualified lookup, as I'll discuss in my next post.

Here's a quick update for those following along :)

One of the things I discussed with Pavel last week was hooking up the qualified lookup API in SwiftLexicalLookup to the compiler to ensure that both implementations produce the same results. Looking more closely at the compiler, I noticed that the C++ implementation uses two main currency types: DeclNameRef and ValueDecl. Namely, the main qualified-lookup methods I'm trying to test against are:

/// Looks for the requested `member` name in the given nominal type declarations.
bool lookupQualified(ArrayRef<NominalTypeDecl *> types, DeclNameRef member,
                     SourceLoc loc, NLOptions options,
                     SmallVectorImpl<ValueDecl *> &decls) const;

/// Looks for given `member` name in the given `Type`.
bool lookupQualified(Type type, DeclNameRef member,
                     SourceLoc loc, NLOptions options,
                     SmallVectorImpl<ValueDecl *> &decls) const;

Contrary to the SwiftLexicalLookup unqualified-lookup API, the member isn't described using an identifier, but using a DeclNameRef. From my understanding, declaration name reference is basically an optional module selector + an identifier + argument names, e.g., MyModule::myFunc(arg1:arg2:). For now, we don't worry about the module selector, so the advantage of using DeclNameRef over a simple Identifier is that we can match against argument names. Here's a simple example:

struct MyStruct {
  func myFunc(name1: Int) { ... }
  func myFunc(name2: Int) { ... }
}

If we used just the myFunc identifier for lookup, we'd have to return both declarations. However, looking up the myFunc(name1:) name in MyStruct unambiguously returns the first declaration.

Further, ValueDecl gives us a more specific lookup result compared to a generic Decl. A value declaration is any declaration with a name that can evaluate to some value. This includes types, function-like declarations, storage declarations, enum-case elements, and macro declarations (we'll handle macros later):

struct MyType {}  // name "MyType" with type `MyType.Type`
func myFunc() {}  // name "myFunc()" with type `() -> Void`
let binding: Int  // name "binding" with type `Int`
enum A { case a } // name "a" with type `() -> A`

// Macros are special
@freestanding macro file = ...  // name `#file`; unresolved type

Coming back to Swift land, you'll notice that my initial proposal using DeclGroup for lookup is very close to the first C++ lookupQualified overload that accepts nominal types. For now, I aim to open a PR that introduces this nominal-type-based lookup with Swift analogs of the DeclNameRef and ValueDecl types.


For those who want to get into the nitty-gritty, here's what my version of DeclNameRef looks like:

/// A macro reference is either freestanding or attached
enum MacroReference: Hashable {
  case freestanding
  case attached
}
/// The arguments eleemnts used for lookup.
/// A `nil` identifier indicates a `_` or nonexistent label, e.g. 
/// `init(_ param: Int)` or `case myCase(Int)`.
typealias DeclNameArgs = [Identifier?]

indirect enum DeclNameRef: Hashable, CustomDebugStringConvertible {
  /// An identifier (macro or not), possibly with argument names
  case identifier(macro: MacroReference? = nil, identifier: Identifier, args: DeclNameArgs? = nil)

  /// Note that it's illegal to reference a subscript, e.g., `myValue[arg1:arg2:]` 
  /// without actually calling it.
  case `subscript`(args: DeclNameArgs)

  /// An explicit reference to init. E.g. `MyType.init` or `MyType.init(arg:)`
  case `init`(args: DeclNameArgs?)

  /// Only tooling can reference deinits.
  case `deinit`

  /// An unnamed call could refer to an `init` in a static context
  /// or a `callAsFunction` if it's an instance. It could also
  /// refer to `@dynamicallyCallable` or `@dynamicMemberLookup`.
  /// See below.
  case unnamedCall(args: DeclNameArgs)

  /// Other keywords
  case `self`, `Type`, `Protocol`
}

This type is clearly more explicit and more complex than its C++ counterpart, so I'll explain my rationale for all these different cases. The .identifier case is basically the C++ type. The .subscript, .init and .deinit cases are separate because the declarations they reference use keywords rather than user-defined identifiers. The compiler still groups these cases with .identifier but uses a getKind() method to distinguish between them. I separated the cases to capture each one's particular semantics: deinit never takes an argument list, while init and subscript always do (even if empty).

Lastly, unnamedCall is the weirdest one. I think it's best if I demonstrate its intended usage with an example:

struct MyStruct {
  init(initArg: Int) {}
  func callAsFunction(callArg: Int) {}
}

let _ = (MyModule::MyStruct(initArg: 5))(callArg: 0)

Suppose some tool or the type-checker tries to evaluate the expression above. We see the function call expression MyModule::MyStruct(initArg: 5) so we look up MyStruct(initArg:) in MyModule and we get back struct MyStruct of type MyStruct.Type. Since the arguments aren't part of MyStruct, we look up (initArg:) in MyStruct.Type and we get back init(initArg: Int) with type (Int) -> MyStruct. Since the init is applied, we could assume the nested function call produces a value of type MyStruct. Then, we look for (callArg:) in MyStruct and get back our function declaration. In the last two lookups, our DeclNameRef didn't have an identifier, so we would have used unnamedCall. Since the only way for there not to be an identifier is during a non-macro function call, I made unnamedCall its own case.

While there's more to say about ValueDecl, I'm still actively tinkering with its interface, so I'll leave that for a future post.

EDIT: Here's the relevant PR.


As always, I'm looking for suggestions and feedback. Does this seem like a reasonable direction for the qualified-lookup API? Do you envision any problems when it comes to validating these against the compiler's implementation? Thanks for reading this!

1 Like

I wanted to suggest storing only a single field inside .lookForSupertypes and allowing clients to determine which supertypes a type conforms to. Clients could then easily parse the result array using a switch statement and look up the relevant fields in the syntax nodes themselves.

var results: [QualifiedLookupResult]

for result in results {
  switch result {
  // ...
  case .lookForSupertypes(of: actorSyntax as ActorDeclSyntax):
    // Handle actor supertypes
  case .lookForSupertypes(of: classSyntax as ClassDeclSyntax):
    // Handle class supertypes
  case .lookForSupertypes(of: structSyntax as StructDeclSyntax):
    // Handle struct supertypes
  // ...
  }
}

My point was that, rather than referring to InheritanceClauseSyntax and GenericWhereClauseSyntax separately, you could simply store their parent node. I think introducing Supertype is a great idea if the goal is to make things more explicit. What do you think about storing it alongside the syntax node reference in lookForSupertypes, e.g. lookForSupertypes([Supertype], of: DeclGroupSyntax)? I'm not sure whether this idea is still relevant given the changes in your newer post, though.

I see your point that we could be more explicit about which implicit names are actually possible during unqualified lookup. I think that could be a good tradeoff, considering that the only downside would be a duplicated definition of implicit self.

That's a very good point about backticks, but we already have a way to differentiate this behavior in LookupName without needing to define a separate LookupResult kind for implicit names. I see that, in your new post, this would instead be covered in qualified lookup by DeclNameRef though.

Would it make sense to further break down DeclNameRef.identifier? I think a more expressive set of enum cases would be beneficial. For example, splitting

.identifier(
  macro: MacroReference? = nil,
  identifier: Identifier,
  args: DeclNameArgs? = nil
)

into

.macro(
  MacroReference,
  identifier: Identifier,
  args: DeclNameArgs? = nil
)

.identifier(
  Identifier,
  args: DeclNameArgs? = nil
)

would eliminate one optional value.

Good point. Yeah, I think I’ll move away from the generic-parameter and inheritance-clause fields and directly provide the underlying declaration instead.

Right. I forgot that "`Self`" shadows "Self" when looking up type declarations. This is another example for anyone else reading along:

struct A {
  typealias `Self` = Int

  func f(x: Self) { let _: Int = x } //  Self  == Int
}

Yes, splitting the macro case would be more ergonomic. I’ll update the declaration-name types.


Also, I had a question regarding the testImplicitSelf test. Here's a simplified version:

7️⃣extension A {
    struct B {
        2️⃣func foo() {
            let x: 3️⃣Self = 4️⃣self
        }
    }
}

In this code, when looking up Self at the :three: marker, we expect the following results:

.lookForMembers(StructDeclSyntax.self),    // Look in type `A.B`
.fromScope(ExtensionDeclSyntax.self,       // Implicit "`Self`" in `A`
           expectedNames: [NameExpectation.implicit(.Self("7️⃣"))]),
.lookForGenericParameters,                 // Generic parameters of `A`
.lookForMembers(ExtensionDeclSyntax.self), // Look in type `A`

My question is: why isn't there an implicit "Self" introduced at the struct b scope? Namely, I would expect lookup to search for:

  1. types named "`Self`" in A.B, and then for

    Example
    // Members in `A.B` takes priority over generic parameter
    struct A1<Self> {}
    
    extension A1 {
        struct B1 {
            typealias `Self` = Int
            static func f(x: Self) {
                let _: Int = x      // Self -> alias for Int
            }
        }
    }
    
  2. generic parameters of A, and then for

    Example
     // Generic parameter over members in `A`
    struct A2<Self> {}
    
    extension A2 {
        typealias `Self` = Int
        struct B2 {
            static func f(x: Self) {} 
        }
    }
    A2<String>.B2.f(x: "") // Self -> generic parameter
    
  3. types named "`Self`" in A, and then for

    Example
    // Members in `A` over implicit `Self`
    struct A3 {
        typealias `Self` = Int
    }
    
    extension A3 {
        struct B3 {
            static func f(x: Self) {
                let _: Int = x // Self -> alias for Int
            } 
        } 
    }
    
  4. implicit Self in A.B, and finally for

    Example
    // Implicit `Self` last
    struct A4 {}
    
    extension A4 {
         struct B4 {
            static func f(x: Self) {
                let _: A4.B4 = x // Self --implicit-> A.B
            } 
        }
    }
    
  5. (implicit Self in A)

This is related to quirky behavior of ASTScope. At first, the new unqualified lookup did introduce Self in nominal type decls, but when we started testing against the compiler, it turned out the original implementation only introduces Self in extensions and protocols.

I think it's a good idea to keep both implementations as close to each other as possible for the time being, but, once it's possible to fully replace ASTScope, this behavior should probably be reconsidered.

What would you say about also reflecting this behavior in qualified lookup for now and introducing the missing Self in there? This way both queries would remain consistent with each other. Also, it will be interesting to see once you're able to tap into the compiler implementation whether those assumptions we've made still hold up.

1 Like

Yeah, this is probably the best course of action. I’ll see what the compiler outputs and try to replicate that. I think I’ll include a flag for the time being to toggle between the two modes (introducing Self for all nominal types vs just protocols and extensions).