[GSoC 2026] Qualified Name Lookup for swift-syntax

Hi everyone! My name is Ronit, and I'm a Computer Science student in my final year at the University of North Texas. I've been working with Swift for several years and have developed a strong interest in how programming languages are structured and tooled. I'm one of the top 10 contributors to swift-containers, which has given me hands-on experience contributing to the swift-syntax ecosystem and working within the Swift open source community. GSoC would be a great opportunity for me to go deeper into the compiler tooling layer — and the qualified name lookup project is exactly the kind of challenge I've been looking for.

The project I'm most interested in is Qualified name lookup for swift-syntax. I've been following Jakub's excellent GSoC 2024 work on SwiftLexicalLookup closely, and I think building the qualified lookup counterpart on top of it is a natural and exciting next step.

Background: Unqualified vs. Qualified Lookup

Before diving in, it's worth briefly distinguishing unqualified lookup from qualified lookup, since SwiftLexicalLookup already handles the former.

Unqualified lookup resolves a bare name like foo by traversing the lexical scope chain upward code blocks, function parameters, closures, nominal type member scopes, and so on until a match is found. This is what SwiftLexicalLookup implements beautifully as a stateless, syntax-tree-based API.

Qualified lookup is different. It resolves a dotted reference like A.f by asking: "given that we know A is a specific named type, what members named f are visible on A?" In Swift, this is non-trivial because A's visible members come from multiple sources:

  • The primary type declaration of A (struct, class, enum, actor, protocol)

  • All extensions of A in the same module

  • Extensions of A in imported modules that satisfy access control

  • Inherited members from A's superclass (for classes)

  • Default implementations and requirements from protocols that A conforms to

A source tool that only has unqualified lookup like the current SwiftLexicalLookup can tell you where to look for members (it surfaces a .lookInMembers prompt at nominal type boundaries), but it cannot actually perform that member lookup. The goal of this project is to implement the other half: a SwiftQualifiedLookup library that, given a type name and a member name, returns all matching declarations visible on that type.

The Problem in Detail

Consider this code:

protocol Drawable {
    func draw()
    func area() -> Double
}

extension Drawable {
    func area() -> Double { 0.0 } // default implementation
}

class Shape: Drawable {
    func draw() { ... }
}

class Circle: Shape {
    var radius: Double
    func area() -> Double { .pi * radius * radius }
}

extension Circle {
    func description() -> String { "Circle(r=\(radius))" }
}

A source tool trying to resolve Circle.area needs to know that:

  1. Circle has its own declaration of area() — this should shadow the protocol default

  2. Circle inherits draw() from Shape

  3. Circle has description() from its extension

None of this is answerable by SwiftLexicalLookup alone. We need a symbol table scoped to the nominal type Circle that aggregates members from all of the above sources in the correct precedence order.

This is the problem qualified name lookup solves.

Proposed Design

I'm thinking about the project in three layered pieces: a symbol table builder, a lookup query API, and integration with SwiftLexicalLookup.

1. Symbol Table: NominalTypeMemberTable

The foundation of the library is a per-type symbol table that indexes the visible members of a given nominal type declaration. A rough sketch of the structure:

public struct NominalTypeMemberTable {
    // Maps member name → all declarations with that name
    private var members: [String: [DeclSyntax]] = [:]

    public init(for type: some DeclGroupSyntax, in tree: SourceFileSyntax) {
        // 1. Collect members from primary declaration
        // 2. Walk extensions in the same file
        // 3. Walk superclass chain (for ClassDeclSyntax)
        // 4. Walk protocol conformances for default implementations
    }

    public func lookup(_ name: String) -> [DeclSyntax] {
        members[name, default: []]
    }
}

The table is built eagerly for a given type and source file context. Because swift-syntax operates on a single file at a time (it has no cross-file type information), the table will collect all same-file extensions and note protocol conformances and superclass relationships by name, leaving cross-module resolution for clients to handle.

2. Lookup Query API: qualifiedLookup(_:on:in:)

On top of the symbol table, we expose a clean query API consistent with how SwiftLexicalLookup is structured — stateless and syntax-node-oriented:

// Resolve `Circle.description` given a reference node in the tree
let results = someNode.qualifiedLookup("description", on: "Circle", in: sourceFile)

The result type mirrors LookupResult from SwiftLexicalLookup, partitioning results by the scope of introduction:

public enum QualifiedLookupResult {
    // Member declared directly on the named type
    case member(DeclSyntax, introducedIn: DeclGroupSyntax)
    // Member found in an extension of the named type
    case extensionMember(DeclSyntax, introducedIn: ExtensionDeclSyntax)
    // Inherited member from superclass
    case inheritedMember(DeclSyntax, from: ClassDeclSyntax)
    // Default implementation from protocol extension
    case protocolDefault(DeclSyntax, from: ExtensionDeclSyntax)
}

Separating result kinds like this gives clients (IDEs, refactoring tools, linters) the information they need to reason about shadowing, inheritance, and protocol conformance in one pass.

3. Integration with SwiftLexicalLookup

The .lookInMembers result produced by SwiftLexicalLookup is exactly the hook where SwiftQualifiedLookup plugs in. When an unqualified lookup hits a nominal type boundary and surfaces a .lookInMembers prompt, clients can hand control to SwiftQualifiedLookup to finish the resolution. I want to make this handoff feel seamless:

// In SwiftLexicalLookup result processing
for result in someNode.lookup("area", with: config) {
    switch result {
    case .lookInMembers(let typeSyntax):
        // Hand off to qualified lookup
        let memberResults = typeSyntax.qualifiedLookup("area", in: sourceFile)
        // ... process memberResults
    default:
        // ... process unqualified results
    }
}

This gives users of the combined API a complete name resolution path for Swift source code — purely from the syntax tree, with no compiler dependency.

Right now, any source tool built on swift-syntax — whether it's a linter, an IDE plugin, a refactoring tool, or a code generator — has to either reimplement member lookup logic themselves or depend on SourceKit for it. That's a heavy dependency to pull in just to answer "what members does this type have?"

A self-contained SwiftQualifiedLookup library changes that. Together with SwiftLexicalLookup, it gives the swift-syntax ecosystem a complete, compiler-independent name resolution stack. That's the same goal motivating the overall SwiftLexicalLookup project — and this is the missing piece to complete it.

A few things I know will need careful handling:

Extensions with where clauses. extension Array where Element: Equatable introduces members only for constrained instantiations. The library will need to record these constraints and surface them in the result so clients can apply them correctly.

Protocol inheritance chains. A type conforming to P where P: Q: R needs to surface default implementations from all three protocol extensions, in the right precedence order.

Operator declarations. Operators can be declared in extensions and are often used as qualified members. They need to be indexed by their operator symbol, not just an identifier name.

Access control. private and fileprivate members introduced in extensions in the same file are only visible under specific conditions. The symbol table should record access modifiers and let clients filter appropriately.

I'd love to get feedback from the @xedin especially around the result data structure and whether the NominalTypeMemberTable approach makes sense given how swift-syntax is currently structured. Looking forward to the discussion!

Ronit

1 Like

This write-up looks fine to me. I mostly wanna push you in a simpler direction because I think the core of this is quite valuable for SwiftSyntax to have as you note. And it’s better we leave off the complicated bits and bobs if we can’t properly implement them.

Some things to bear in mind: Because you won’t be performing full qualified lookup here I think it’s important out of the gate that this library come with a qualifier like “lexical qualified lookup” to distinguish it from the operation you’d need to perform if you were writing something like a compiler or a full-project refactoring pass in an IDE.

We need a symbol table scoped to the nominal type Circle that aggregates members from all of the above sources in the correct precedence order.

You don’t actually! And to save yourself quite a bit of complexity I would actually shy away from using this as the foundation of your library. You “just” need a bit of name resolution and the ability to traverse the members of a given declaration. Adding a symbol table behind that amortizes the cost of repeated lookups and does sit underneath something like direct member lookup in a proper compiler but here I think a stateless implementation as a first pass is more desirable.

in the correct precedence order.

You don’t have to worry about ordering the results, or pruning them for that matter. It should be up to the caller to filter for access control, availability, generic constraints, etc. and sorting the output.

qualifiedLookup(_:on:in:)

I think the on parameter is superfluous given that you’re intending to use the syntax node you issue qualifiedLookup(_:on:in:) as the root of the search. I’d get rid of it.

QualifiedLookupResult

Which case do lookups rooted at generic parameters fall under? Is it fair to say that a witness satisfied by a generic parameter’s bounding protocols or superclass is a “member” of that type?

a complete name resolution path for Swift source code

complete, compiler-independent name resolution stack.

This is not quite right. You will get limited, lexical, file-scoped qualified name resolution out of this library. Which is totally fine! That’s a very useful facility to have.

Extensions with where clauses

I would say don’t worry about this. You will not have the ability to resolve any of the bounds involved. It should be up to a client that can resolve those bounds to do this filtering themselves. Just return the member in context as you have here.

Protocol inheritance chains.

Don’t worry about precedence. Clients can deal with shadowing and inheritance problems themselves.

Access control.

Don’t worry about filtering for access control. Access modifiers require semantic analysis to fully compute as well as a more well-fleshed-out generic scope implementation that SwiftSyntax doesn’t have. This is another “leave it to clients” thing.

2 Likes