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