[SwiftLexicalLookup][GSoC] Add proper guard scope and implicit name lookup#2748
Conversation
Conflicts: Sources/SwiftLexicalLookup/ScopeImplementations.swift
… internal. Format.
Conflicts: Tests/SwiftLexicalLookupTest/NameLookupTests.swift
Conflicts: Sources/SwiftLexicalLookup/Configurations/LookupConfig.swift Sources/SwiftLexicalLookup/LookupName.swift
ahoppen
left a comment
There was a problem hiding this comment.
- Could you update
name->identifierin 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 |
| 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.
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.
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.
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.
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.
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.
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( |
There was a problem hiding this comment.
Why do we need currentChunk at all? Couldn’t we add the names directly to result?
There was a problem hiding this comment.
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.
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.
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.
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) |
There was a problem hiding this comment.
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.
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.
|
@swift-ci please test |
|
@swift-ci please test Windows |
DougGregor
left a comment
There was a problem hiding this comment.
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.
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.
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.
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.
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.
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 = 1should 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.
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
guardhandling and implicit name lookup.IntroducingToSequentialParentScopeSyntaxkind introduce their lookup results to theirSequentialScopeSyntaxparents. Currently, the onlyIntroducingToSequentialParentScopeSyntaxisguardscope and the onlySequentialScopeSyntaxs are code block and file scope (the latter one usesSequentialScopeSyntaxproperties depending on the configuration).guard … elseclause specifiesskipSequentialIntroductionFromproperty in the newLookupStateobject passed betweenlookupfunctions. 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.LookupNameimplicitcase. Associated with it, a new enumLookupImplicitNameKinddescribes all possible implicit names that could be referenced during lexical name lookup.TypeScopeSyntaxdefines behavior of type declaration and extensions scopes. In the subsequent PRs could be used to add generic parameter handling and possibly other common behavior.