-
Notifications
You must be signed in to change notification settings - Fork 440
[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
Conversation
Conflicts: Sources/SwiftLexicalLookup/ScopeImplementations.swift
… internal. Format.
Conflicts: Tests/SwiftLexicalLookupTest/NameLookupTests.swift
Conflicts: Sources/SwiftLexicalLookup/Configurations/LookupConfig.swift Sources/SwiftLexicalLookup/LookupName.swift
There was a problem hiding this 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?
…equential scopes. Fix partitioning with non matching results from `guard` scopes.
@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) |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 LookupName
s 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( |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 LookupResult
s 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) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 LookupName
s within LookupResult
s 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
.
@swift-ci please test |
@swift-ci please test Windows |
There was a problem hiding this 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 { |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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).
|
@swift-ci please test |
1 similar comment
@swift-ci please test |
Not sure what failed here now. I see in the console output all tests passed
Though there are multiple „message decoding errors” from LSP like
Could this be it has something to do with the new |
It crashed with
It’s a non-deterministic failure on Linux, though I haven’t seen it for months. Unrelated to your changes though. |
@swift-ci please test Linux |
func refersTo(_ lookedUpIdentifier: Identifier) -> Bool { | ||
guard let identifier else { return false } | ||
return identifier == lookedUpIdentifier |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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 } |
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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?
Alright, let's go ahead and merge this! |
This PR introduces proper
guard
handling and implicit name lookup.IntroducingToSequentialParentScopeSyntax
kind introduce their lookup results to theirSequentialScopeSyntax
parents. Currently, the onlyIntroducingToSequentialParentScopeSyntax
isguard
scope and the onlySequentialScopeSyntax
s are code block and file scope (the latter one usesSequentialScopeSyntax
properties depending on the configuration).guard … else
clause specifiesskipSequentialIntroductionFrom
property in the newLookupState
object passed betweenlookup
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.LookupName
implicit
case. Associated with it, a new enumLookupImplicitNameKind
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.