GSoC 2024 - Lexical Scopes for swift-syntax project idea

Hi everyone!
I’m Jakub and I’m a Bachelor Computer Science and Engineering student at TU Delft. Over the last year I’ve read up about compilers and I’ve found parsers especially interesting. I’ve been using Swift for the past 4 years and can easily call it my favorite language.

That’s why I’ve been thinking about participating in GSoC this year. The lexical scopes for swift-syntax project specifically caught my attention. I’ve looked through the swift-syntax codebase and documentation. I also had a look at the mentioned C++ implementation of scopes. I have some ideas and made a simple demo for you to check out.

The idea

Swift-Syntax uses CodeBlockSyntax to represent chunks of code forming different scopes. In practice, those can be closures, if-statement bodies, class/struct bodies etc. Introducing lexical scopes could be implemented with a tree structure using CodeBlockSyntax nodes for creating separate scope nodes. The declarations could be then accessed recursively by looking up the closest CodeBlockSyntax ancestor. In order to do this, we could introduce a scope property.

extension CodeBlockSyntax {
    var scope: BlockScope? {
        // ...
    }
}

Implementation

All scopes would conform to a common protocol Scope which would provide methods to retrieve reference declarations and their parent scopes.

/// Protocol describing a general scope.
protocol Scope {
    /// Parent scope.
    var parent: Scope? { get }
    
    /// Returns all the declarations available in the scope berore the specified absolute position. Sorted by position.
    func getAllDeclarations(before position: AbsolutePosition) -> [Declaration]
    
    /// Returns the declaration passed reference refers to.
    func getDeclaration(of declarationReference: DeclReferenceExprSyntax) -> Declaration?
}

At the root of this tree would be a GlobalScope (property of SourceFileSyntax). All scopes represented by CodeBlockSyntax would be of type BlockScope. It would have two properties: startDeclarations i.e. declarations passed from it’s parent scope and localDeclarations containing variables introduced within the scope body (making it somewhat similar to the BraceStmtScope from the aforementioned C++ implementation). It would also define an introducedVariables() method that would be overridden by subclasses to introduce additional scope-specific variables (like function parameters, newValue in setters etc.). This way we would also implement variable shadowing.

/// Represents scope within brackets. Should be further subclassed to provide specific functionality
class BlockScope: Scope {
    /// Code block represented by this scope.
    var codeBlockSyntax: CodeBlockSyntax?
    
    /// Parent of this scope.
    var parent: Scope? {
        getParent(syntax: codeBlockSyntax?.parent)
    }
    
    /// Variables passed from the parent scope.
    var startDeclarations: [Declaration] {
        guard let codeBlockSyntax, let parent else { return [] }
        
        return parent.getAllDeclarations(before: codeBlockSyntax.position)
    }
    
    /// Variables declared inside the scope.
    var localDelcarations: [Declaration] {
        guard let codeBlockSyntax else { return [] }
        
        return codeBlockSyntax.statements.compactMap { statement in
            if let variableDeclarationSyntax = statement.item.as(VariableDeclSyntax.self) {
                return Declaration(variableDeclarationSyntax)
            } else {
                return nil
            }
        }
    }

    // ...

    /// Returns all the declarations available in the scope berore the specified absolute position. Sorted by position.
    func getAllDeclarations(before position: AbsolutePosition) -> [Declaration] {
        return (startDeclarations.filter({ startDeclaration in
            !introducedVariables().contains(where: { parameter in
                startDeclaration.name == parameter.name
            })
        }) + introducedVariables() +
            localDelcarations.filter({ $0.position < position })).sorted(by: { $0.position < $1.position })
    }
    
    /// Variables introduced by this scope. Should be overriden by subclass. Could be function parameters, optional bindings, implicit newValue in a setter etc.
    func introducedVariables() -> [Declaration] {
        []
    }
}

BlockScope would be then subclassed to create desired behavior depending on the type of scope i.e. passing function parameters, optional bindings etc.

/// Scope of a function.
class FunctionScope: BlockScope {
    /// Syntax of the function.
    var functionDeclarationSyntax: FunctionDeclSyntax
    
    /// Parameters introduced in the function signature.
    var parameters: [Declaration] {
        functionDeclarationSyntax.signature.parameterClause.parameters.map({ Declaration($0) })
    }
    
    // ...
    
    /// Returns variables introduced by this scope (parameters).
    override func introducedVariables() -> [Declaration] {
        parameters
    }
}

All this combined would provide a nice foundation for a scalable API for handling scopes. I’d like to especially ask @Douglas_Gregor if the idea is somewhat sensible and aligns with the expected outcomes of the project so far. What else should I (re)consider? What do you expect in the proposal?

Demo

You can check out my concept and run it here. I also added some more details to the README.md.

Wrapping up…

I would appreciate any help or guidance. I don’t have much experience with open source, so I hope GSoC will introduce me to this incredible world and will jump start my involvement in the Swift project :)

Hope you like the idea,
Jakub

4 Likes