Skip to content

[SwiftLexicalLookup][GSoC] Add proper guard scope and implicit name lookup #2748

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 33 commits into from
Aug 6, 2024

Conversation

MAJKFL
Copy link
Contributor

@MAJKFL MAJKFL commented Jul 23, 2024

This PR introduces proper guard handling and implicit name lookup.

  • Scopes of new internal IntroducingToSequentialParentScopeSyntax kind introduce their lookup results to their SequentialScopeSyntax parents. Currently, the only IntroducingToSequentialParentScopeSyntax is guard scope and the only SequentialScopeSyntaxs are code block and file scope (the latter one uses SequentialScopeSyntax properties depending on the configuration).
  • Lookup from within guard … else clause specifies skipSequentialIntroductionFrom property in the new LookupState object passed between lookup functions. I think it would be great to make this more opaque for the client, so I'd love to hear suggestions/other ideas how we could represent and pass the state between lookup methods.
  • Implicit name handling works through a new LookupName implicit case. Associated with it, a new enum LookupImplicitNameKind describes all possible implicit names that could be referenced during lexical name lookup.
  • TypeScopeSyntax defines behavior of type declaration and extensions scopes. In the subsequent PRs could be used to add generic parameter handling and possibly other common behavior.
  • This PR also includes some not addressed changes proposed in [SwiftLexicalLookup][GSoC] Add initial name lookup functionality #2719

MAJKFL added 4 commits July 25, 2024 09:29
Conflicts:
	Sources/SwiftLexicalLookup/Configurations/LookupConfig.swift
	Sources/SwiftLexicalLookup/LookupName.swift
@MAJKFL MAJKFL requested a review from DougGregor as a code owner July 30, 2024 09:58
Copy link
Member

@ahoppen ahoppen left a comment

Choose a reason for hiding this comment

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

  • Could you update name -> identifier in various doc comment for which you changed the parameter name?

@MAJKFL MAJKFL requested review from ahoppen and DougGregor August 1, 2024 13:16
@DougGregor
Copy link
Member

@swift-ci please test

@_spi(Experimental) public var identifier: Identifier? {
switch self {
case .identifier(let syntax, _):
return Identifier(syntax.identifier)
return Identifier(syntax.identifier) ?? Identifier(syntax.identifier.text)
Copy link
Member

Choose a reason for hiding this comment

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

Do we need ?? Identifier(syntax.identifier.text) for self and Self? If so, I would prefer to handle these two keywords specifically. I’m not sure if treating a keyword the same as an identifier is safe in general (I certainly wouldn’t have expected so, see my previous example).

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 added new LookupName kinds: self and Self. As for now it's only really used in closure captures, but I agree it's nice to have a separate representation for both of those.

var sequentialItems = [CodeBlockItemSyntax]()
var encounteredNonDeclaration = false

for codeBlockItem in statements {
Copy link
Member

Choose a reason for hiding this comment

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

I'm a little confused myself as to why we're gathering declarations up to the first non-declaration, then gathering everything else as code block items, because I don't consider the first non-declaration to be special in Swift's lookup rules.

Does this mean that we should remove the memberBlockUpToLastDecl lookup kind?


import Foundation

@_spi(Experimental) public struct LookupState {
Copy link
Member

Choose a reason for hiding this comment

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

I agree that we should remove LookupState altogether.

protocol IntroducingToSequentialParentScopeSyntax: ScopeSyntax {
/// Returns names matching lookup that should be
/// handled by it's parent sequential scope.
func introducesToSequentialParent(
Copy link
Member

Choose a reason for hiding this comment

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

I find it a little surprising that ScopeSyntax has an API that returns all names introduce in its scope (introducedNames) while this API contains a filter. I think it would be more consistent if this also returned all the names introduced to the parent scope and then be named namesIntroducedToParentScope. As far as I can tell it wouldn’t need to take any parameters at all then and could be a computed property as well.

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 think that was poor naming on my part as introducesToSequentialParent worked more like lookup producing [LookupResult] to be interleaved with results of the sequential parent, rather than introducedNames that just return all LookupNames available at that scope. I renamed introducesToSequentialParent to less misleading lookupFromSequentialParent and added namesIntroducedToSequentialParent that closely resembles introducedNames of a normal scope.

result += introducedResults
} else {
// Extract new names from encountered node.
currentChunk += LookupName.getNames(
Copy link
Member

Choose a reason for hiding this comment

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

Why do we need currentChunk at all? Couldn’t we add the names directly to result?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Again, I think it was a bit poor naming on my side as result was an array of LookupResult. I renamed it now to results.

As I briefly mentioned in this comment, we can't add the names directly to results as we need to partition them based on results obtained from IntroducingToSequentialParentScopeSyntax.

Copy link
Member

Choose a reason for hiding this comment

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

Ah, I see. Could you add that as a comment to the code?

from: codeBlockItem.item,
accessibleAfter: codeBlockItem.endPosition
).filter { introducedName in
checkName(identifier, refersTo: introducedName, at: origin)
Copy link
Member

Choose a reason for hiding this comment

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

I guess we could also move the filter all the way to the end instead of duplicating it.

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 we need to create multiple LookupResults here, I think the only other possible place to move filtering to, would be when creating a new result from collected currentChunk. Either way, we'd need to traverse the same amount of items when filtering so the time complexity would be the same, but this way we can maintain smaller sizes of currentChunk.

result.append(getResults(currentChunk))
}

return result.reversed() + lookupInParent(for: identifier, at: origin, with: config, state: state)
Copy link
Member

Choose a reason for hiding this comment

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

Instead of reversing the result here (which requires a copy of the array), have you considered changing the semantics of the result to say a later element overrides earlier elements? I think I would also find that more intuitive because it matches the source order.

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 we already represent results from inner-most to outer-most, I think it makes most sense to reverse it. Like in this example:

func foo() {
  let a = 1 // <-- Code block scope
  let b = 2

  guard let b = a else { return }

  let a = 3 // <-- Code block scope
  let b = 4

  guard let a = b else { return } // <-- Guard scope

  print(a, b) // <-- lookup for a
}

I think the order of results should be: guard scope and exactly one code block scope result (as the first guard is not relevant for a lookup).

The order of LookupNames within LookupResults is the way you described actually. In the result from code block scope the order is exactly: let a = 1 and then let a = 3.

@MAJKFL MAJKFL requested a review from ahoppen August 2, 2024 14:49
@DougGregor
Copy link
Member

@swift-ci please test

@DougGregor
Copy link
Member

@swift-ci please test Windows

Copy link
Member

@DougGregor DougGregor left a comment

Choose a reason for hiding this comment

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

While we have some open questions about the handling of top-level code, I'm happy with where this pull request is now and I think we should go ahead and land it once CI passes.

var sequentialItems = [CodeBlockItemSyntax]()
var encounteredNonDeclaration = false

for codeBlockItem in statements {
Copy link
Member

Choose a reason for hiding this comment

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

Maybe? There's definitely a difference here for the source file scope when top-level code is involved, and we need a way to model it. I think we should leave this in place for now and we can rename/revise as we learn more.

at syntax: SyntaxProtocol,
with config: LookupConfig,
state: LookupState,
createResultsForThisScopeWith getResults: ([LookupName]) -> (LookupResult)
Copy link
Member

Choose a reason for hiding this comment

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

Part of the reason for having fromFileScope be separate is because clients need to do extra work to do lookups into other source files and in visible imports from that scope, so it's both "here's what I found in this file" and "you need to go look elsewhere, too". I'm not totally settled on what the right answer is for dealing with file scope, so I'd rather keep thinking about it (but not block this PR on a resolution).

@DougGregor
Copy link
Member

/home/build-user/swift-syntax/Tests/SwiftSyntaxTest/IdentifierTests.swift:42:43: error: cannot use optional chaining on non-optional value of type 'RawIdentifier'
40 |     let token = TokenSyntax(stringLiteral: "sometoken")
41 |     withExtendedLifetime(token) { token in
42 |       XCTAssertEqual(token.identifier?.raw?.name, SyntaxText("sometoken"))
   |                                           `- error: cannot use optional chaining on non-optional value of type 'RawIdentifier'
43 |     }
44 |   }

@DougGregor
Copy link
Member

@swift-ci please test

1 similar comment
@DougGregor
Copy link
Member

@swift-ci please test

@MAJKFL
Copy link
Contributor Author

MAJKFL commented Aug 5, 2024

Not sure what failed here now. I see in the console output all tests passed

Test Suite 'All tests' passed at 2024-08-02 23:13:38.842
	 Executed 640 tests, with 24 tests skipped and 0 failures (0 unexpected) in 448.763 (448.763) seconds

Though there are multiple „message decoding errors” from LSP like

Failed to decode message: MessageDecodingError(code: LanguageServerProtocol.ErrorCode(rawValue: -32602), message: "missing expected parameter: string", id: nil, messageKind: LanguageServerProtocol.MessageDecodingError.MessageKind.notification)

Could this be it has something to do with the new Identifier initializers?

@ahoppen
Copy link
Member

ahoppen commented Aug 6, 2024

It crashed with

SourceKitLSPPackageTests.xctest: /home/build-user/swift/stdlib/public/Concurrency/TaskPrivate.h:803: void swift::AsyncTask::PrivateStorage::complete(AsyncTask *): Assertion `oldStatus.getInnermostRecord() == NULL' failed.

It’s a non-deterministic failure on Linux, though I haven’t seen it for months. Unrelated to your changes though.

@DougGregor
Copy link
Member

@swift-ci please test Linux

Comment on lines +168 to +170
func refersTo(_ lookedUpIdentifier: Identifier) -> Bool {
guard let identifier else { return false }
return identifier == lookedUpIdentifier
Copy link
Member

Choose a reason for hiding this comment

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

Does this method provide any value anymore? I think you can remove the guard let identifier else { return false } and then all that’s left is identifier == lookedUpIdentifier.

}
}

for codeBlockItem in itemsWithoutNamedDecl {
Copy link
Member

Choose a reason for hiding this comment

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

I see. Could you add that explanation as a comment? Also, I think we could still have a single loop and then gather the type declarations and normal declarations and then concatenate them. Not sure if that’s cleaner.


I still think it’s a little odd because if you think about the following

func foo() {
  let x = 1
  print(x)
  class x {}
}

And then consider a declaration as a re-declaration if there already exists a declaration with the same name in the scope, then let x is the invalid redeclaration of class x where you would consider it the other way round. I don’t have a great suggestion for a solution though…

}

for codeBlockItem in itemsWithoutNamedDecl {
guard codeBlockItem.position < origin else { break }
Copy link
Member

Choose a reason for hiding this comment

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

isAccessible uses <= while this uses <, which is inconsistent.

I did think about whether the lookup position should always be compared with <= or < but I think <= is the better choice because when you do a jump-to-definition at the start of a variable definition, I would expect to go to the definition itself, same if you do it at the character one after the name start.

Ie.

let |f|oo = 1

should resolve to foo at both | positions.

result += introducedResults
} else {
// Extract new names from encountered node.
currentChunk += LookupName.getNames(
Copy link
Member

Choose a reason for hiding this comment

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

Ah, I see. Could you add that as a comment to the code?

@DougGregor
Copy link
Member

Alright, let's go ahead and merge this!

@DougGregor DougGregor merged commit 2f0b28d into swiftlang:main Aug 6, 2024
3 checks passed
@MAJKFL
Copy link
Contributor Author

MAJKFL commented Aug 7, 2024

Thanks for merging! I included suggested changes by @ahoppen in #2782.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants