Skip to content

Instantly share code, notes, and snippets.

@filip-sakel
Created September 1, 2026 02:29
Show Gist options
  • Select an option

  • Save filip-sakel/61310ed71e950b4bf8b8f9037b373929 to your computer and use it in GitHub Desktop.

Select an option

Save filip-sakel/61310ed71e950b4bf8b8f9037b373929 to your computer and use it in GitHub Desktop.
GSoC_2026_Forum_Post

Forum Post

GSoC 2026: Qualified Name Lookup for swift-syntax

Introduction

As part of Google Summer of Code 2026, I implemented a preliminary version of qualified name lookup in swift-syntax under the mentorship of Pavel Yaskevich (@xedin). Building on Jakub Florek’s GSoC 2024 unqualified-name-lookup project, this effort exposes syntactic queries formerly confined to the Swift compiler as an easy-to-use API in the swift-syntax package. Now, swift-syntax can find definitions of types and type members, like a simplified version of an editor’s “jump to definition” function. Namely, developers of build tools, such as linters, code generators and macros, can determine which declaration a TypeSyntax refers to, or find that type’s members.

Motivation

The proposed qualified lookup narrows the gap between SwiftSyntax’s currently limited parsing facilities and more advanced AST queries, which build-tool developers usually outsource to the SourceKitten library, a tool primarily geared to Integrated Development Environments.

The 2024 project’s SwiftLexicalLookup swift-syntax module is already powering certain features in build tools such as *SwiftLint* and swift-java. I hope these projects and additional build tools can leverage the new qualified lookup queries. To illustrate the novel functionality, consider this code:

struct MyView {
  var model: MyModel // <- Resolve 'Model' and find members named 'sayHi()'
}
final class MyModel {
  func sayHi() { print("Hi") } // <- Returns the function declaration
}

In the example above, SwiftLexicalLookup can now resolve the MyModel type and find members matching the name “sayHi()”.

Implementation Journey

Qualified lookup ended up as three overarching problems: resolving a type syntax to a declaration, finding the declarations directly contained by that type, and binding extensions to the type they extend.

Preliminary API

I started by creating a forum thread to share project updates and get the community’s feedback. In this thread, Alex Hoppen and Jakub Florek suggested some changes to the API I had sketched in my GSoC proposal, and I ended up making significant changes to the proposed interface as a result.

This API was strongly inspired by the existing unqualified lookup interface, where we have a lookup method on a syntax type returning a result enum. However, while prototyping this version, my mentor serendipitously suggested I look into how we could validate results against the compiler. So, as I was tinkering with the compiler and through weekly video calls with Pavel, I realized I shouldn’t mimic the unqualified-lookup API, since unqualified lookup cares about a lot more types of names. For instance, unqualified lookup surfaces closure parameters, but qualified lookup doesn’t care; the type checker will find their type and then call qualified lookup. In fact, the highest-level query that qualified lookup performs is supertype lookup: searching in superclasses and protocols we conform to.

Thus, while following @rintaro’s great advice and implementing two PRs that help us validate our outputs against the compiler’s existing name-lookup facilities, I reached back out to the community to suggest two fundamental changes. For one, qualified lookup surfaces more members, such as initializers, deinitializers, and subscripts, so I proposed DeclNameReference as a more expressive version of Identifier. Secondly, rather than returning a multi-case enum result like unqualified lookup, my mentor and I stripped the result down to an array of ValueDeclSyntax, a type representing the possible member nodes of a type. These changes landed in a PR at the end of June, which I summarize here — where DeclGroupSyntax is any nominal-type declaration (struct, enum, class, actor, protocol):

// == Simplified final interface ==

/// Find named member declarations in the given group declaration.
extension DeclGroupSyntax {
  func findDirectMembers(
    name: DeclNameReference?,
    kind memberKind: MemberKind = .default, // instance members by default
    configuredRegions: ConfiguredRegions? = nil
  ) -> [ValueDeclSyntax]
}

/// A module + a name, e.g., 'Swift::print()'
struct DeclNameReference {
  let moduleIdentifier: Identifier?
  let baseName: DeclNameReferenceBase
}
/// A name, e.g., 'print()'
enum DeclNameReferenceBase {
  public enum MacroReference: Hashable {
    case freestanding
    case attached
  }

  // E.g., `debugDescription`, `hash(into:)`
  case identifier(identifier: Identifier, arguments: DeclNameArguments? = nil)
  case `init`(arguments: DeclNameArguments?)
  case `deinit`
  // E.g. `[_:]`, `[_:default:]`
  case `subscript`(arguments: DeclNameArguments)

  /// An unnamed call could refer to an init in a static context
  /// or a `callAsFunction` if it's an instance.
  case unnamedCall(arguments: DeclNameArguments)

  // ...
}

/// Syntax like `func`, `init`, `deinit`, `subscript`, variable-identifier
/// patterns, enum-case elements, and type declarations (will discuss below).
struct ValueDeclSyntax: SyntaxProtocol {}

So given some StructDeclSyntax, all these convoluted types let us look for precise members:

// ┏━━ Searching in `struct MyType`
struct MyType {
  func f() {} // Returned when looking up `f` or `f()`

  // Handles `#if`
  #if true
  subscript(a: A, label b: B) {} // Returned when looking up `subscript(_:label:)`
  #endif
}

Having now completed the preliminary API, I thought I was close to wrapping up the project: I just needed to iron out corner cases and implement supertype lookup.

Type Resolution

While working on the compiler validation and strategies for supertype lookup, I continued to have weekly meetings, in which he elucidated the need for qualified lookup to understand type syntax. Namely, the compiler has methods that find members of some type syntax, not nominal-type declarations. So, instead of looking for f() in struct MyType, the compiler may ask us to find f() in MyModule::MyType. Further, superclasses and protocol conformances use type syntax. For instance, in the conformance struct MyCollection: Swift.Collection, what type does the Swift.Collection syntax refer to? Thankfully, during a PR review, @slava_pestov suggested changes to simplify the compiler’s name-lookup machinery, which sent me looking deep through the codebase. That’s when I stumbled upon the documentation I needed: an explanation of “structural type resolution,” the compiler’s way of construing type syntax.

Type resolution is highly recursive and, ergo, complex. Consider the following example:

struct A {
  typealias B = A

  func f(_: MyModule::A.B) {} // <- Resolve `A.B`
}

This process is tedious, but I want to illustrate the rampant recursion. We start from the leaf type syntax, the identifier type MyModule::A. Because we have a module selector, we only look up at file scope, and trivially discover struct A {}. We’ve resolved the leaf node. Now, using the aforementioned findDirectMembers API, we look for a member type “B.” We find typealias B = A and recursively evaluate the syntax A. We invoke unqualified lookup, and since there’s no nested A.A type (declaration-group scope), the type syntax finally evaluates to the file-scope struct A {} declaration.

It took me some time to figure out what each recursive sub-request needs to handle and how they all fit together, but I eventually got type resolution in the example above to work. After implementing basic type resolution, I turned my attention to corner cases like type redeclarations, compositions, and cyclical aliases, such as:

typealias A = B
typealias B = A

Throughout my type-resolution journey, I also submitted bug reports for random issues (one, two, and three) and implemented thorough logging in my code. By mid-July, I shared a rough design diagram in the aforementioned forum thread, and I was feeling good about type resolution. So, I was almost done with the project, right?

Extension Binding

As I kept working on the compiler validation, I noticed that the C++ qualified lookup has a handy NominalTypeDecl class that exposes a type’s main nominal-type declaration and all extensions “bound” to it. But swift-syntax is, well, syntactic and doesn’t offer this information. Therefore, we need to find the extensions of each nominal type whose declaration we resolve in the previous step.

Unfortunately for me, Swift is extremely expressive, so finding a type’s extensions is challenging (related post). Firstly, Swift’s type aliases make finding extensions a global problem. To resolve “A,” we can’t just find extension A {}; we also need, say, extension AliasedA {}. Instead, we have to bind all accessible extensions to determine which extensions actually resolve to A. Moreover, Swift lets us extend a type declared in an extension, making extension binding incremental. In other words, one-by-one, we must resolve each unbound extension, admit it into a dependency graph, and evict any dependent extensions.

I spent the remaining six weeks of GSoC designing the dependency graph that powers extension binding. The implementation is complex but the principle is simple: the type-dependency graph always upholds its invariants. The main invariant is that we can’t admit an extension that binds to a type that already-admitted extensions depend on. To help me visualize the incremental binding, I implemented an intuitive debug description that uses SwiftDiagnostics to show information in the code. Consider the following code that trips up the compiler:

struct A {}
//     `- Type 'MyModule::A'
extension A.B { struct A {} }
extension A { typealias B = A }

At first, our graph sees only struct A {}. Starting from extension A.B {}, we admit the extension to the graph with a failed type, since A currently has no member B, and record that this extension depends on type A. The debug description now resembles this:

struct A {}
//     `- Type 'MyModule::A'
extension A.B { struct A {} }
//        |- Resolved type: failed (type 'A' has no member type 'B')
//        `- Depends on type 'A' > member 'B"
extension A { typealias B = A }

Moving on to the second extension, extension A {} resolves successfully to A, but extension A.B {} depends on A. To maintain the graph’s invariant, we first evict extension A.B {}, and then admit extension A {} into the graph.

struct A {}
//     `- Type 'MyModule::A'
extension A.B { struct A {} }
extension A { typealias B = A }
//        `- Resolved type: 'MyModule::A'

Finally, when we try to re-admit the evicted extension A.B {}, we run into a problem. If we bind extension A.B {} to “MyModule::A,” then the extension will depend on struct A {}, a type the extension itself introduces. Hence, we diagnose an extension-cycle error, which is more specific than the compiler’s current warning.

struct A {}
//     `- Type 'MyModule::A'
extension A.B { struct A {} }
//        |- Resolved type: failed (extension depends on itself)
//        `- Depends on type 'A' > member 'B"
extension A { typealias B = A }
//        `- Resolved type: 'MyModule::A'

This admission-eviction dance took me some time to wrap my head around, but it crucially maintains qualified-lookup state always consistent.

Pavel and I identified a few more extension edge cases, such as nested types inside extensions and type redeclarations, after which the initial version of qualified lookup was ready. I didn’t handle supertype lookup as I’d initially hoped, but I’m very satisfied with our extension-binding system. Additionally, we don’t support access-control modifiers and internal/imported-module lookup, as both were out of scope.

Current State

Working Example

struct MyView { // <- In `struct MyView {}`, find members named 'model'
  #if true
  var model: Model // <- In type 'Model', find members named 'sayHi()'
  #endif
}
extension MyView {
  typealias Model = MyModel
}
final class MyModel {
  func sayHi() { print("Hi") }
}

Here, a user is requesting that:

  1. In the struct declaration MyView, we find a member named “model,” and
  2. In the type represented by the syntax Model, we find a member called “sayHi()”.

As you can see, we handle #if, bind extension MyView correctly, and finally follow the Model alias to find the function MyModel/sayHi(). I’m very excited to see what build tools can do with SwiftLexicalLookup’s qualified-lookup queries.

Pull Requests Timeline

Most of my work is and will end up in the swift-syntax repository’s SwiftLexicalLookup module. Further, following instructive review from Swift maintainers, I’ve landed some PRs that should make validating qualified lookup against the compiler easier. I’ve compiled a list of all the merged and pending PRs:

Merged Name Repo and Link
Jun 24 Preliminary Qualified-Lookup API swift-syntax #3346
Jun 28 Convert NLOptions to an OptionSet swift #90090
Jul 9 Merge NameLookupOptions into NLOptions swift #90440
Jul 27 Add Syntax Type Helpers for Member-Type Lookup swift-syntax #3378
Jul 31 Add inline-lexical-assertion helpers for qualified-lookup tests swift-syntax #3390
Aug 12 Implement the first step of type resolution swift-syntax #3399
Aug 18 Make function types into errors in partial type resolution swift-syntax #3401
Aug 20 Add unqualified type lookup swift-syntax #3402
Pending Scaffold SymbolTable as a driver of type resolution swift-syntax #3404

Future Directions

I still need to upstream a lot of the type-resolution and extension-binding features. Then, the new API needs to be formally proposed through the RFC process.

I think validating against the compiler’s outputs is also crucial for correctness and consistency. Further, supertype lookup, filtering by access-control modifiers, expanding attached macros, and performing lookup in the current and imported modules would make for incredibly useful additions. More abstractly, this library could also be benchmarked and optimized for performance and memory usage. I think an arena allocator for ephemeral allocations during type resolution and extension binding is a promising direction.

Reflection

I found designing a novel system for qualified lookup quite challenging. The design space was big, and I was overwhelmed at first. I had to remind myself to start small and implement a minimum viable product: the preliminary findDirectMembers API. I also learned to ask my mentor and other community members for guidance, who, in turn, helped me simplify the target interface to the ValueDeclSyntax result type. Of course, ValueDeclSyntax came to fruition because I was working on related issues in the compiler, receiving helpful comments from PR reviews, and reading the documentation as part of my research. With ValueDeclSyntax in my toolbox, the rest of the project became dramatically simpler. Hence, I discovered that system design is about sketching ideas by drawing on others’ perspectives, working on adjacent problems, and reading documentation.

The same idea of focusing on the minimum viable product helped me devise the different pieces of type resolution. Namely, I found it incredibly helpful to write tests at different levels of complexity and to focus on getting the simple tests to pass first. By writing code that passed the simple tests, the basic components of type resolution, like handling leaf type-syntax nodes, naturally emerged. Granted, some tests were inappropriate for that level of the system — for instance, I had tests with extensions while still working on type resolution. However, test-driven development gave me a concrete target and tangibly marked my successes.

Finally, mapping out extension binding taught me the importance of assertions and comprehensive logging. It’s really easy to mess up extension binding. So, by investing in extensive and intuitive logging, I gained a lot of visibility into what my code was doing. What’s more, assertions helped me identify erroneous code faster. Especially when working with extensions that contain nested types, breaking up each graph operation into smaller parts—like “remove this nested type” or “unbind this type’s extensions”— and enforcing invariants at every step tremendously simplified debugging.

In sum, I now better understand the challenges of system design and have developed communication, organizational, and code-reasoning skills to aid me in the future.

Acknowledgements

Solving an interesting problem in a real-world codebase with Pavel’s exceptional mentorship made GSoC an incredibly fulfilling experience. Moreover, I want to thank Jakub Florek, Douglas Gregor, Alex Hoppen, Rintaro Ishizaki, and Slava Pestov for participating in the community discussions, providing helpful pointers, and thoroughly reviewing my code. I extend my utmost gratitude to Pavel, the Swift organizers, and the GSoC admins for this opportunity. I hope to continue working on Swift and related open-source projects in the future.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment