Skip to content

[SwiftLexicalLookup][GSoC] Add initial name lookup functionality #2719

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Jul 23, 2024
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

import SwiftSyntax

/// Specifies how names should be introduced at the file scope.
@_spi(Experimental) public enum FileScopeHandlingConfig {
/// Default behavior. Names introduced sequentially like in member block
/// scope up to the first non-declaration after and including which,
/// the declarations are treated like in code block scope.
case memberBlockUpToLastDecl
/// File scope behaves like member block scope.
case memberBlock
/// File scope behaves like code block scope.
case codeBlock
}
23 changes: 23 additions & 0 deletions Sources/SwiftLexicalLookup/Configurations/LookupConfig.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

import Foundation

@_spi(Experimental) public struct LookupConfig {
/// Specifies behaviour of file scope.
/// `memberBlockUpToLastDecl` by default.
public var fileScopeHandling: FileScopeHandlingConfig = .memberBlockUpToLastDecl

public init(fileScopeHandling: FileScopeHandlingConfig = .memberBlockUpToLastDecl) {
self.fileScopeHandling = fileScopeHandling
}
}
43 changes: 43 additions & 0 deletions Sources/SwiftLexicalLookup/IdentifiableSyntax.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

import SwiftSyntax

/// Syntax node that can be refered to with an identifier.
public protocol IdentifiableSyntax: SyntaxProtocol {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like the kind of thing that we should sink down into SwiftSyntax itself. It can be a refactoring we apply later, when we're more sure about the design.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's iterate with this protocol some more. We might want to sink it down into SwiftSyntax and make the NamedDeclSyntax protocol inherit from it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(this doesn't have to happen now)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll keep adding the protocol to more syntax nodes like FunctionParameterSyntax and GenericParameterSyntax in the coming PRs. Also, would it be a good idea to soon fix the problem with ClosureCaptureSyntax so we can complete implementation of IdentifiableSyntax?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the fix with ClosureCaptureSyntax should be a separate PR from this one entirely, since it will involve changing the syntax tree. I don't specifically mind whether it goes before or after this PR; we can fix it up either way. (But we shouldn't sink this protocol down into SwiftSyntax until we fix the issue)

var identifier: TokenSyntax { get }
}

extension IdentifierPatternSyntax: IdentifiableSyntax {}

extension ClosureParameterSyntax: IdentifiableSyntax {
@_spi(Experimental) public var identifier: TokenSyntax {
secondName ?? firstName
}
}

extension ClosureShorthandParameterSyntax: IdentifiableSyntax {
@_spi(Experimental) public var identifier: TokenSyntax {
name
}
}

extension ClosureCaptureSyntax: IdentifiableSyntax {
@_spi(Experimental) public var identifier: TokenSyntax {
/* Doesn't work with closures like:
_ = { [y=1+2] in
print(y)
}
*/
expression.as(DeclReferenceExprSyntax.self)!.baseName
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't going to handle all of the possible kinds of captures. For example, here are some captures that aren't a DeclReferenceExprSyntax:

{ [x=y+1, weak self] in print(x) }

I think the best result here would involve reworking the syntax tree and parser to provide more structure for the parsed closure captures, so there's a single node describing the captured name that we can make IdentifiableSyntax.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This won't work for all closure captures. For example:

{ [x=y+1, weak self] in ... }

I suspect the right answer is to rework the syntax tree so that the syntax structure always provides something that we can make an IdentifiableSyntax without the !.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(this doesn't have to happen now)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As for now, I left a comment next to the force unwrap so we don't forget about it. Do you have already an idea how could we alter the syntax tree?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I think we want different syntactic forms for captures like x, x=<expr>, and weak x, where the x in every case is just the syntax for the capture name. It will make it a lot easier to identify the names introduced by captures.

}
}
122 changes: 122 additions & 0 deletions Sources/SwiftLexicalLookup/LookupName.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

import SwiftSyntax

@_spi(Experimental) public enum LookupName {
/// Identifier associated with the name.
/// Could be an identifier of a variable, function or closure parameter and more
case identifier(IdentifiableSyntax, accessibleAfter: AbsolutePosition?)
/// Declaration associated with the name.
/// Could be class, struct, actor, protocol, function and more
case declaration(NamedDeclSyntax, accessibleAfter: AbsolutePosition?)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I expect that we'll need another case here eventually for "implicit" names, such as the error introduced in

do {
  try something()
} catch { // the catch clause introduces the name `error`.
}

This can, of course, come later.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the suggestion. I'll introduce implicit names with the next PR.


/// Syntax associated with this name.
@_spi(Experimental) public var syntax: SyntaxProtocol {
switch self {
case .identifier(let syntax, _):
syntax
case .declaration(let syntax, _):
syntax
}
}

/// Introduced name.
@_spi(Experimental) public var identifier: Identifier? {
switch self {
case .identifier(let syntax, _):
Identifier(syntax.identifier)
case .declaration(let syntax, _):
Identifier(syntax.name)
}
}

/// Point, after which the name is available in scope.
/// If set to `nil`, the name is available at any point in scope.
var accessibleAfter: AbsolutePosition? {
switch self {
case .identifier(_, let absolutePosition), .declaration(_, let absolutePosition):
absolutePosition
}
}

/// Checks if this name was introduced before the syntax used for lookup.
func isAccessible(at lookedUpSyntax: SyntaxProtocol) -> Bool {
guard let accessibleAfter else { return true }
return accessibleAfter <= lookedUpSyntax.position
}

/// Checks if this name refers to the looked up phrase.
func refersTo(_ lookedUpName: String) -> Bool {
guard let name = identifier?.name else { return false }
return name == lookedUpName
}

/// Extracts names introduced by the given `from` structure.
static func getNames(from syntax: SyntaxProtocol, accessibleAfter: AbsolutePosition? = nil) -> [LookupName] {
switch Syntax(syntax).as(SyntaxEnum.self) {
case .variableDecl(let variableDecl):
variableDecl.bindings.flatMap { binding in
getNames(from: binding.pattern, accessibleAfter: accessibleAfter)
}
case .tuplePattern(let tuplePattern):
tuplePattern.elements.flatMap { tupleElement in
getNames(from: tupleElement.pattern, accessibleAfter: accessibleAfter)
}
case .valueBindingPattern(let valueBindingPattern):
getNames(from: valueBindingPattern.pattern, accessibleAfter: accessibleAfter)
case .expressionPattern(let expressionPattern):
getNames(from: expressionPattern.expression, accessibleAfter: accessibleAfter)
case .sequenceExpr(let sequenceExpr):
sequenceExpr.elements.flatMap { expression in
getNames(from: expression, accessibleAfter: accessibleAfter)
}
case .patternExpr(let patternExpr):
getNames(from: patternExpr.pattern, accessibleAfter: accessibleAfter)
case .optionalBindingCondition(let optionalBinding):
getNames(from: optionalBinding.pattern, accessibleAfter: accessibleAfter)
case .matchingPatternCondition(let matchingPatternCondition):
getNames(from: matchingPatternCondition.pattern, accessibleAfter: accessibleAfter)
case .functionCallExpr(let functionCallExpr):
functionCallExpr.arguments.flatMap { argument in
getNames(from: argument.expression, accessibleAfter: accessibleAfter)
}
case .guardStmt(let guardStmt):
guardStmt.conditions.flatMap { cond in
getNames(from: cond.condition, accessibleAfter: cond.endPosition)
}
default:
if let namedDecl = Syntax(syntax).asProtocol(SyntaxProtocol.self) as? NamedDeclSyntax {
handle(namedDecl: namedDecl, accessibleAfter: accessibleAfter)
} else if let identifiable = Syntax(syntax).asProtocol(SyntaxProtocol.self) as? IdentifiableSyntax {
handle(identifiable: identifiable, accessibleAfter: accessibleAfter)
} else {
[]
}
}
}

/// Extracts name introduced by `IdentifiableSyntax` node.
private static func handle(identifiable: IdentifiableSyntax, accessibleAfter: AbsolutePosition? = nil) -> [LookupName]
{
if identifiable.identifier.text != "_" {
return [.identifier(identifiable, accessibleAfter: accessibleAfter)]
} else {
return []
}
}

/// Extracts name introduced by `NamedDeclSyntax` node.
private static func handle(namedDecl: NamedDeclSyntax, accessibleAfter: AbsolutePosition? = nil) -> [LookupName] {
[.declaration(namedDecl, accessibleAfter: accessibleAfter)]
}
}
39 changes: 39 additions & 0 deletions Sources/SwiftLexicalLookup/LookupResult.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

import SwiftSyntax

/// Represents resul
@_spi(Experimental) public enum LookupResult {
/// Scope and the names that matched lookup.
case fromScope(ScopeSyntax, withNames: [LookupName])
/// File scope and names that matched lookup.
case fromFileScope(SourceFileSyntax, withNames: [LookupName])

/// Associated scope.
@_spi(Experimental) public var scope: ScopeSyntax? {
switch self {
case .fromScope(let scopeSyntax, _):
scopeSyntax
case .fromFileScope(let fileScopeSyntax, _):
fileScopeSyntax
}
}

/// Names that matched lookup.
@_spi(Experimental) public var names: [LookupName] {
switch self {
case .fromScope(_, let names), .fromFileScope(_, let names):
names
}
}
}
Loading