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
Ain the same module -
Extensions of
Ain imported modules that satisfy access control -
Inherited members from
A's superclass (for classes) -
Default implementations and requirements from protocols that
Aconforms 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:
-
Circlehas its own declaration ofarea()— this should shadow the protocol default -
Circleinheritsdraw()fromShape -
Circlehasdescription()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