diff --git a/Sources/SwiftLexicalLookup/CMakeLists.txt b/Sources/SwiftLexicalLookup/CMakeLists.txt index 3a73e794869..a2e96af80d6 100644 --- a/Sources/SwiftLexicalLookup/CMakeLists.txt +++ b/Sources/SwiftLexicalLookup/CMakeLists.txt @@ -8,6 +8,7 @@ add_swift_syntax_library(SwiftLexicalLookup IdentifiableSyntax.swift + LookupCache.swift LookupName.swift LookupResult.swift SimpleLookupQueries.swift diff --git a/Sources/SwiftLexicalLookup/LookupCache.swift b/Sources/SwiftLexicalLookup/LookupCache.swift new file mode 100644 index 00000000000..862b0c8a18e --- /dev/null +++ b/Sources/SwiftLexicalLookup/LookupCache.swift @@ -0,0 +1,101 @@ +//===----------------------------------------------------------------------===// +// +// 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 + +/// Unqualified lookup cache. Should be used when performing +/// large sequences of adjacent lookups to maximise performance. +public class LookupCache { + /// Cached results of `ScopeSyntax.lookupParent` calls. + /// Identified by `SyntaxIdentifier`. + private var ancestorResultsCache: [SyntaxIdentifier: [LookupResult]] = [:] + /// Cached results of `SequentialScopeSyntax.sequentialLookup` calls. + /// Identified by `SyntaxIdentifier`. + private var sequentialResultsCache: [SyntaxIdentifier: [LookupResult]] = [:] + /// Looked-up scope identifiers during cache accesses. + private var hits: Set = [] + + private let dropMod: Int + private var evictionCount = 0 + + /// Creates a new unqualified lookup cache. + /// `drop` parameter specifies how many eviction calls will be + /// ignored before evicting not-hit members of the cache. + /// + /// Example cache eviction sequences (s - skip, e - evict): + /// - `drop = 0` - `e -> e -> e -> e -> e -> ...` + /// - `drop = 1` - `s -> e -> s -> s -> e -> ...` + /// - `drop = 3` - `s -> s -> s -> e -> s -> ...` + /// + /// - Note: `drop = 0` effectively maintains exactly one path of cached results to + /// the root in the cache (assuming we evict cache members after each lookup in a sequence of lookups). + /// Higher the `drop` value, more such paths can potentially be stored in the cache at any given moment. + /// Because of that, a higher `drop` value also translates to a higher number of cache-hits, + /// but it might not directly translate to better performance. Because of a larger memory footprint, + /// memory accesses could take longer, slowing down the eviction process. That's why the `drop` value + /// could be fine-tuned to maximize the performance given file size, + /// number of lookups, and amount of available memory. + public init(drop: Int = 0) { + self.dropMod = drop + 1 + } + + /// Get cached ancestor results for the given `id`. + /// `nil` if there's no cache entry for the given `id`. + /// Adds `id` and ids of all ancestors to the cache `hits`. + func getCachedAncestorResults(id: SyntaxIdentifier) -> [LookupResult]? { + guard let results = ancestorResultsCache[id] else { return nil } + hits.formUnion(results.map(\.scope.id)) + hits.insert(id) + return results + } + + /// Set cached ancestor results for the given `id`. + /// Adds `id` to the cache `hits`. + func setCachedAncestorResults(id: SyntaxIdentifier, results: [LookupResult]) { + hits.insert(id) + ancestorResultsCache[id] = results + } + + /// Get cached sequential lookup results for the given `id`. + /// `nil` if there's no cache entry for the given `id`. + /// Adds `id` to the cache `hits`. + func getCachedSequentialResults(id: SyntaxIdentifier) -> [LookupResult]? { + guard let results = sequentialResultsCache[id] else { return nil } + hits.insert(id) + return results + } + + /// Set cached sequential lookup results for the given `id`. + /// Adds `id` to the cache `hits`. + func setCachedSequentialResults(id: SyntaxIdentifier, results: [LookupResult]) { + hits.insert(id) + sequentialResultsCache[id] = results + } + + /// Removes all cached entries without a hit, unless it's prohibited + /// by the internal drop counter (as specified by `drop` in the initializer). + /// The dropping behavior can be disabled for this call with the `bypassDropCounter` + /// parameter, resulting in immediate eviction of entries without a hit. + public func evictEntriesWithoutHit(bypassDropCounter: Bool = false) { + if !bypassDropCounter { + evictionCount = (evictionCount + 1) % dropMod + guard evictionCount != 0 else { return } + } + + for key in Set(ancestorResultsCache.keys).union(sequentialResultsCache.keys).subtracting(hits) { + ancestorResultsCache.removeValue(forKey: key) + sequentialResultsCache.removeValue(forKey: key) + } + + hits = [] + } +} diff --git a/Sources/SwiftLexicalLookup/Scopes/CanInterleaveResultsLaterScopeSyntax.swift b/Sources/SwiftLexicalLookup/Scopes/CanInterleaveResultsLaterScopeSyntax.swift index 5c21d49c7ee..abac5ca1632 100644 --- a/Sources/SwiftLexicalLookup/Scopes/CanInterleaveResultsLaterScopeSyntax.swift +++ b/Sources/SwiftLexicalLookup/Scopes/CanInterleaveResultsLaterScopeSyntax.swift @@ -20,6 +20,7 @@ protocol CanInterleaveResultsLaterScopeSyntax: ScopeSyntax { _ identifier: Identifier?, at lookUpPosition: AbsolutePosition, with config: LookupConfig, + cache: LookupCache?, resultsToInterleave: [LookupResult] ) -> [LookupResult] } diff --git a/Sources/SwiftLexicalLookup/Scopes/FunctionScopeSyntax.swift b/Sources/SwiftLexicalLookup/Scopes/FunctionScopeSyntax.swift index 1b2d22c2b8e..81822f60c34 100644 --- a/Sources/SwiftLexicalLookup/Scopes/FunctionScopeSyntax.swift +++ b/Sources/SwiftLexicalLookup/Scopes/FunctionScopeSyntax.swift @@ -30,7 +30,8 @@ extension FunctionScopeSyntax { @_spi(Experimental) public func lookup( _ identifier: Identifier?, at lookUpPosition: AbsolutePosition, - with config: LookupConfig + with config: LookupConfig, + cache: LookupCache? ) -> [LookupResult] { var thisScopeResults: [LookupResult] = [] @@ -39,6 +40,7 @@ extension FunctionScopeSyntax { identifier, at: position, with: config, + cache: cache, propagateToParent: false ) } @@ -47,7 +49,8 @@ extension FunctionScopeSyntax { + lookupThroughGenericParameterScope( identifier, at: lookUpPosition, - with: config + with: config, + cache: cache ) } } diff --git a/Sources/SwiftLexicalLookup/Scopes/GenericParameterScopeSyntax.swift b/Sources/SwiftLexicalLookup/Scopes/GenericParameterScopeSyntax.swift index dd6e3036129..d0f1bfdc875 100644 --- a/Sources/SwiftLexicalLookup/Scopes/GenericParameterScopeSyntax.swift +++ b/Sources/SwiftLexicalLookup/Scopes/GenericParameterScopeSyntax.swift @@ -40,18 +40,21 @@ protocol GenericParameterScopeSyntax: ScopeSyntax {} @_spi(Experimental) public func lookup( _ identifier: Identifier?, at lookUpPosition: AbsolutePosition, - with config: LookupConfig + with config: LookupConfig, + cache: LookupCache? ) -> [LookupResult] { return defaultLookupImplementation( identifier, at: lookUpPosition, with: config, + cache: cache, propagateToParent: false ) + lookupBypassingParentResults( identifier, at: lookUpPosition, - with: config + with: config, + cache: cache ) } @@ -76,16 +79,22 @@ protocol GenericParameterScopeSyntax: ScopeSyntax {} private func lookupBypassingParentResults( _ identifier: Identifier?, at lookUpPosition: AbsolutePosition, - with config: LookupConfig + with config: LookupConfig, + cache: LookupCache? ) -> [LookupResult] { guard let parentScope else { return [] } if let parentScope = Syntax(parentScope).asProtocol(SyntaxProtocol.self) as? WithGenericParametersScopeSyntax { - return parentScope.returningLookupFromGenericParameterScope(identifier, at: lookUpPosition, with: config) + return parentScope.returningLookupFromGenericParameterScope( + identifier, + at: lookUpPosition, + with: config, + cache: cache + ) } else { - return lookupInParent(identifier, at: lookUpPosition, with: config) + return lookupInParent(identifier, at: lookUpPosition, with: config, cache: cache) } } } diff --git a/Sources/SwiftLexicalLookup/Scopes/IntroducingToSequentialParentScopeSyntax.swift b/Sources/SwiftLexicalLookup/Scopes/IntroducingToSequentialParentScopeSyntax.swift index 9163ffa7359..a755a102edd 100644 --- a/Sources/SwiftLexicalLookup/Scopes/IntroducingToSequentialParentScopeSyntax.swift +++ b/Sources/SwiftLexicalLookup/Scopes/IntroducingToSequentialParentScopeSyntax.swift @@ -21,6 +21,7 @@ protocol IntroducingToSequentialParentScopeSyntax: ScopeSyntax { func lookupFromSequentialParent( _ identifier: Identifier?, at lookUpPosition: AbsolutePosition, - with config: LookupConfig + with config: LookupConfig, + cache: LookupCache? ) -> [LookupResult] } diff --git a/Sources/SwiftLexicalLookup/Scopes/NominalTypeDeclSyntax.swift b/Sources/SwiftLexicalLookup/Scopes/NominalTypeDeclSyntax.swift index 32a1d2cca52..8bdb279e5f9 100644 --- a/Sources/SwiftLexicalLookup/Scopes/NominalTypeDeclSyntax.swift +++ b/Sources/SwiftLexicalLookup/Scopes/NominalTypeDeclSyntax.swift @@ -34,16 +34,18 @@ extension NominalTypeDeclSyntax { @_spi(Experimental) public func returningLookupFromGenericParameterScope( _ identifier: Identifier?, at lookUpPosition: AbsolutePosition, - with config: LookupConfig + with config: LookupConfig, + cache: LookupCache? ) -> [LookupResult] { if let inheritanceClause, inheritanceClause.range.contains(lookUpPosition) { - return lookupInParent(identifier, at: lookUpPosition, with: config) + return lookupInParent(identifier, at: lookUpPosition, with: config, cache: cache) } else if let genericParameterClause, genericParameterClause.range.contains(lookUpPosition) { - return lookupInParent(identifier, at: lookUpPosition, with: config) + return lookupInParent(identifier, at: lookUpPosition, with: config, cache: cache) } else if name.range.contains(lookUpPosition) || genericWhereClause?.range.contains(lookUpPosition) ?? false { - return lookupInParent(identifier, at: lookUpPosition, with: config) + return lookupInParent(identifier, at: lookUpPosition, with: config, cache: cache) } else { - return [.lookForMembers(in: Syntax(self))] + lookupInParent(identifier, at: lookUpPosition, with: config) + return [.lookForMembers(in: Syntax(self))] + + lookupInParent(identifier, at: lookUpPosition, with: config, cache: cache) } } } diff --git a/Sources/SwiftLexicalLookup/Scopes/ScopeImplementations.swift b/Sources/SwiftLexicalLookup/Scopes/ScopeImplementations.swift index 94db0fa4605..93488b51600 100644 --- a/Sources/SwiftLexicalLookup/Scopes/ScopeImplementations.swift +++ b/Sources/SwiftLexicalLookup/Scopes/ScopeImplementations.swift @@ -45,14 +45,16 @@ import SwiftSyntax @_spi(Experimental) public func lookup( _ identifier: Identifier?, at lookUpPosition: AbsolutePosition, - with config: LookupConfig + with config: LookupConfig, + cache: LookupCache? ) -> [LookupResult] { return statements.flatMap { codeBlockItem in if let guardStmt = codeBlockItem.item.as(GuardStmtSyntax.self) { return guardStmt.lookupFromSequentialParent( identifier, at: lookUpPosition, - with: config + with: config, + cache: cache ) } else { return [] @@ -77,13 +79,15 @@ import SwiftSyntax @_spi(Experimental) public func lookup( _ identifier: Identifier?, at lookUpPosition: AbsolutePosition, - with config: LookupConfig + with config: LookupConfig, + cache: LookupCache? ) -> [LookupResult] { sequentialLookup( in: statements, identifier, at: lookUpPosition, - with: config + with: config, + cache: cache ) } } @@ -104,12 +108,13 @@ import SwiftSyntax @_spi(Experimental) public func lookup( _ identifier: Identifier?, at lookUpPosition: AbsolutePosition, - with config: LookupConfig + with config: LookupConfig, + cache: LookupCache? ) -> [LookupResult] { if pattern.range.contains(lookUpPosition) || sequence.range.contains(lookUpPosition) { - return lookupInParent(identifier, at: lookUpPosition, with: config) + return lookupInParent(identifier, at: lookUpPosition, with: config, cache: cache) } else { - return defaultLookupImplementation(identifier, at: lookUpPosition, with: config) + return defaultLookupImplementation(identifier, at: lookUpPosition, with: config, cache: cache) } } } @@ -168,13 +173,15 @@ import SwiftSyntax @_spi(Experimental) public func lookup( _ identifier: Identifier?, at lookUpPosition: AbsolutePosition, - with config: LookupConfig + with config: LookupConfig, + cache: LookupCache? ) -> [LookupResult] { let sequentialResults = sequentialLookup( in: statements, identifier, at: lookUpPosition, with: config, + cache: cache, propagateToParent: false ) @@ -201,7 +208,8 @@ import SwiftSyntax ) } - return sequentialResults + signatureResults + lookupInParent(identifier, at: lookUpPosition, with: config) + return sequentialResults + signatureResults + + lookupInParent(identifier, at: lookUpPosition, with: config, cache: cache) } } @@ -281,12 +289,13 @@ import SwiftSyntax @_spi(Experimental) public func lookup( _ identifier: Identifier?, at lookUpPosition: AbsolutePosition, - with config: LookupConfig + with config: LookupConfig, + cache: LookupCache? ) -> [LookupResult] { if let elseBody, elseBody.range.contains(lookUpPosition) { - return lookupInParent(identifier, at: lookUpPosition, with: config) + return lookupInParent(identifier, at: lookUpPosition, with: config, cache: cache) } else { - return defaultLookupImplementation(identifier, at: lookUpPosition, with: config) + return defaultLookupImplementation(identifier, at: lookUpPosition, with: config, cache: cache) } } } @@ -306,7 +315,8 @@ import SwiftSyntax func lookupAssociatedTypeDeclarations( _ identifier: Identifier?, at lookUpPosition: AbsolutePosition, - with config: LookupConfig + with config: LookupConfig, + cache: LookupCache? ) -> [LookupResult] { let filteredNames = members.flatMap { member in guard member.decl.kind == .associatedTypeDecl else { return [LookupName]() } @@ -351,7 +361,8 @@ import SwiftSyntax func lookupFromSequentialParent( _ identifier: Identifier?, at lookUpPosition: AbsolutePosition, - with config: LookupConfig + with config: LookupConfig, + cache: LookupCache? ) -> [LookupResult] { guard !body.range.contains(lookUpPosition) else { return [] } @@ -361,6 +372,16 @@ import SwiftSyntax return LookupResult.getResultArray(for: self, withNames: filteredNames) } + + @_spi(Experimental) public func lookup( + _ identifier: Identifier?, + at lookUpPosition: AbsolutePosition, + with config: LookupConfig, + cache: LookupCache? + ) -> [LookupResult] { + // We're not using `lookupParent` to not cache the results here. + parentScope?.lookup(identifier, at: lookUpPosition, with: config, cache: cache) ?? [] + } } @_spi(Experimental) extension ActorDeclSyntax: NominalTypeDeclSyntax { @@ -405,7 +426,8 @@ import SwiftSyntax @_spi(Experimental) public func lookup( _ identifier: Identifier?, at lookUpPosition: AbsolutePosition, - with config: LookupConfig + with config: LookupConfig, + cache: LookupCache? ) -> [LookupResult] { if memberBlock.range.contains(lookUpPosition) { let implicitSelf: [LookupName] = [.implicit(.Self(DeclSyntax(self)))] @@ -415,21 +437,26 @@ import SwiftSyntax return LookupResult.getResultArray(for: self, withNames: implicitSelf) + [.lookForGenericParameters(of: self)] - + defaultLookupImplementation(identifier, at: lookUpPosition, with: config, propagateToParent: false) - + [.lookForMembers(in: Syntax(self))] - + lookupInParent(identifier, at: lookUpPosition, with: config) + + defaultLookupImplementation( + identifier, + at: lookUpPosition, + with: config, + cache: cache, + propagateToParent: false + ) + [.lookForMembers(in: Syntax(self))] + + lookupInParent(identifier, at: lookUpPosition, with: config, cache: cache) } else if !extendedType.range.contains(lookUpPosition), let genericWhereClause { if genericWhereClause.range.contains(lookUpPosition) { return [.lookForGenericParameters(of: self)] + [.lookForMembers(in: Syntax(self))] - + defaultLookupImplementation(identifier, at: lookUpPosition, with: config) + + defaultLookupImplementation(identifier, at: lookUpPosition, with: config, cache: cache) } return [.lookForGenericParameters(of: self)] - + defaultLookupImplementation(identifier, at: lookUpPosition, with: config) + + defaultLookupImplementation(identifier, at: lookUpPosition, with: config, cache: cache) } return [.lookForGenericParameters(of: self)] - + lookupInParent(identifier, at: lookUpPosition, with: config) + + lookupInParent(identifier, at: lookUpPosition, with: config, cache: cache) } } @@ -461,13 +488,14 @@ import SwiftSyntax @_spi(Experimental) public func lookup( _ identifier: Identifier?, at lookUpPosition: AbsolutePosition, - with config: LookupConfig + with config: LookupConfig, + cache: LookupCache? ) -> [LookupResult] { guard let parentScope, let canInterleaveLaterScope = Syntax(parentScope).asProtocol(SyntaxProtocol.self) as? CanInterleaveResultsLaterScopeSyntax else { - return defaultLookupImplementation(identifier, at: lookUpPosition, with: config) + return defaultLookupImplementation(identifier, at: lookUpPosition, with: config, cache: cache) } let implicitSelf: [LookupName] = [.implicit(.self(DeclSyntax(self)))] @@ -479,12 +507,14 @@ import SwiftSyntax identifier, at: lookUpPosition, with: config, + cache: cache, propagateToParent: false ) + canInterleaveLaterScope.lookupWithInterleavedResults( identifier, at: lookUpPosition, with: config, + cache: cache, resultsToInterleave: implicitSelf.isEmpty ? [] : [.fromScope(Syntax(self), withNames: implicitSelf)] ) } @@ -536,19 +566,22 @@ import SwiftSyntax @_spi(Experimental) public func lookup( _ identifier: Identifier?, at lookUpPosition: AbsolutePosition, - with config: LookupConfig + with config: LookupConfig, + cache: LookupCache? ) -> [LookupResult] { if body.range.contains(lookUpPosition) || isLookupFromWhereClause(lookUpPosition) { return defaultLookupImplementation( identifier, at: lookUpPosition, - with: config + with: config, + cache: cache ) } else { return lookupInParent( identifier, at: lookUpPosition, - with: config + with: config, + cache: cache ) } } @@ -619,20 +652,23 @@ import SwiftSyntax @_spi(Experimental) public func lookup( _ identifier: Identifier?, at lookUpPosition: AbsolutePosition, - with config: LookupConfig + with config: LookupConfig, + cache: LookupCache? ) -> [LookupResult] { let filteredNamesFromLabel = namesFromLabel.filter { name in checkIdentifier(identifier, refersTo: name, at: lookUpPosition) } if label.range.contains(lookUpPosition) && !isInWhereClause(lookUpPosition: lookUpPosition) { - return config.finishInSequentialScope ? [] : lookupInParent(identifier, at: lookUpPosition, with: config) + return config.finishInSequentialScope + ? [] : lookupInParent(identifier, at: lookUpPosition, with: config, cache: cache) } else if config.finishInSequentialScope { return sequentialLookup( in: statements, identifier, at: lookUpPosition, with: config, + cache: cache, propagateToParent: false ) } else { @@ -641,10 +677,11 @@ import SwiftSyntax identifier, at: lookUpPosition, with: config, + cache: cache, propagateToParent: false ) + LookupResult.getResultArray(for: self, withNames: filteredNamesFromLabel) - + lookupInParent(identifier, at: lookUpPosition, with: config) + + lookupInParent(identifier, at: lookUpPosition, with: config, cache: cache) } } @@ -698,7 +735,8 @@ import SwiftSyntax public func lookup( _ identifier: Identifier?, at lookUpPosition: AbsolutePosition, - with config: LookupConfig + with config: LookupConfig, + cache: LookupCache? ) -> [LookupResult] { var results: [LookupResult] = [] @@ -708,7 +746,8 @@ import SwiftSyntax results = memberBlock.lookupAssociatedTypeDeclarations( identifier, at: lookUpPosition, - with: config + with: config, + cache: cache ) } @@ -725,8 +764,9 @@ import SwiftSyntax identifier, at: lookUpPosition, with: config, + cache: cache, propagateToParent: false - ) + lookInMembers + lookupInParent(identifier, at: lookUpPosition, with: config) + ) + lookInMembers + lookupInParent(identifier, at: lookUpPosition, with: config, cache: cache) } } @@ -791,12 +831,14 @@ extension SubscriptDeclSyntax: WithGenericParametersScopeSyntax, CanInterleaveRe @_spi(Experimental) public func lookup( _ identifier: Identifier?, at lookUpPosition: AbsolutePosition, - with config: LookupConfig + with config: LookupConfig, + cache: LookupCache? ) -> [LookupResult] { lookupWithInterleavedResults( identifier, at: lookUpPosition, with: config, + cache: cache, resultsToInterleave: [] ) } @@ -824,6 +866,7 @@ extension SubscriptDeclSyntax: WithGenericParametersScopeSyntax, CanInterleaveRe _ identifier: Identifier?, at lookUpPosition: AbsolutePosition, with config: LookupConfig, + cache: LookupCache?, resultsToInterleave: [LookupResult] ) -> [LookupResult] { var thisScopeResults: [LookupResult] = [] @@ -833,6 +876,7 @@ extension SubscriptDeclSyntax: WithGenericParametersScopeSyntax, CanInterleaveRe identifier, at: position, with: config, + cache: cache, propagateToParent: false ) } @@ -841,7 +885,8 @@ extension SubscriptDeclSyntax: WithGenericParametersScopeSyntax, CanInterleaveRe + lookupThroughGenericParameterScope( identifier, at: lookUpPosition, - with: config + with: config, + cache: cache ) } } @@ -870,13 +915,14 @@ extension SubscriptDeclSyntax: WithGenericParametersScopeSyntax, CanInterleaveRe @_spi(Experimental) public func lookup( _ identifier: Identifier?, at lookUpPosition: AbsolutePosition, - with config: LookupConfig + with config: LookupConfig, + cache: LookupCache? ) -> [LookupResult] { switch accessors { case .getter(let codeBlockItems): - return sequentialLookup(in: codeBlockItems, identifier, at: lookUpPosition, with: config) + return sequentialLookup(in: codeBlockItems, identifier, at: lookUpPosition, with: config, cache: cache) case .accessors: - return lookupInParent(identifier, at: lookUpPosition, with: config) + return lookupInParent(identifier, at: lookUpPosition, with: config, cache: cache) } } @@ -886,19 +932,21 @@ extension SubscriptDeclSyntax: WithGenericParametersScopeSyntax, CanInterleaveRe _ identifier: Identifier?, at lookUpPosition: AbsolutePosition, with config: LookupConfig, + cache: LookupCache?, resultsToInterleave: [LookupResult] ) -> [LookupResult] { guard let parentScope, let canInterleaveLaterScope = Syntax(parentScope).asProtocol(SyntaxProtocol.self) as? CanInterleaveResultsLaterScopeSyntax else { - return lookupInParent(identifier, at: lookUpPosition, with: config) + return lookupInParent(identifier, at: lookUpPosition, with: config, cache: cache) } return canInterleaveLaterScope.lookupWithInterleavedResults( identifier, at: lookUpPosition, with: config, + cache: cache, resultsToInterleave: resultsToInterleave ) } @@ -933,7 +981,8 @@ extension SubscriptDeclSyntax: WithGenericParametersScopeSyntax, CanInterleaveRe @_spi(Experimental) public func lookup( _ identifier: Identifier?, at lookUpPosition: AbsolutePosition, - with config: LookupConfig + with config: LookupConfig, + cache: LookupCache? ) -> [LookupResult] { if (bindings.first?.accessorBlock?.range.contains(lookUpPosition) ?? false) || shouldIntroduceSelfIfLazy(lookUpPosition: lookUpPosition) @@ -942,10 +991,12 @@ extension SubscriptDeclSyntax: WithGenericParametersScopeSyntax, CanInterleaveRe in: (isMember ? [.implicit(.self(DeclSyntax(self)))] : LookupName.getNames(from: self)), identifier, at: lookUpPosition, - with: config + with: config, + cache: cache ) } else { - return lookupInParent(identifier, at: lookUpPosition, with: config) + // We're not using `lookupParent` to not cache the results here. + return parentScope?.lookup(identifier, at: lookUpPosition, with: config, cache: cache) ?? [] } } @@ -955,13 +1006,14 @@ extension SubscriptDeclSyntax: WithGenericParametersScopeSyntax, CanInterleaveRe _ identifier: Identifier?, at lookUpPosition: AbsolutePosition, with config: LookupConfig, + cache: LookupCache?, resultsToInterleave: [LookupResult] ) -> [LookupResult] { guard isMember else { - return lookup(identifier, at: lookUpPosition, with: config) + return lookup(identifier, at: lookUpPosition, with: config, cache: cache) } - return resultsToInterleave + lookupInParent(identifier, at: lookUpPosition, with: config) + return resultsToInterleave + lookupInParent(identifier, at: lookUpPosition, with: config, cache: cache) } /// Returns `true`, if `lookUpPosition` is in initializer of @@ -1001,7 +1053,8 @@ extension SubscriptDeclSyntax: WithGenericParametersScopeSyntax, CanInterleaveRe func lookupFromSequentialParent( _ identifier: Identifier?, at lookUpPosition: AbsolutePosition, - with config: LookupConfig + with config: LookupConfig, + cache: LookupCache? ) -> [LookupResult] { let clause: IfConfigClauseSyntax? @@ -1020,6 +1073,7 @@ extension SubscriptDeclSyntax: WithGenericParametersScopeSyntax, CanInterleaveRe identifier, at: lookUpPosition, with: config, + cache: cache, ignoreNamedDecl: true, propagateToParent: false ) @@ -1062,4 +1116,14 @@ extension SubscriptDeclSyntax: WithGenericParametersScopeSyntax, CanInterleaveRe @_spi(Experimental) public var scopeDebugName: String { "IfConfigScope" } + + @_spi(Experimental) public func lookup( + _ identifier: Identifier?, + at lookUpPosition: AbsolutePosition, + with config: LookupConfig, + cache: LookupCache? + ) -> [LookupResult] { + // We're not using `lookupParent` to not cache the results here. + parentScope?.lookup(identifier, at: lookUpPosition, with: config, cache: cache) ?? [] + } } diff --git a/Sources/SwiftLexicalLookup/Scopes/ScopeSyntax.swift b/Sources/SwiftLexicalLookup/Scopes/ScopeSyntax.swift index bdac3f27ec4..3e91c4ce04f 100644 --- a/Sources/SwiftLexicalLookup/Scopes/ScopeSyntax.swift +++ b/Sources/SwiftLexicalLookup/Scopes/ScopeSyntax.swift @@ -15,6 +15,11 @@ import SwiftSyntax extension SyntaxProtocol { /// Returns all names that `identifier` refers to at this syntax node. /// Optional configuration can be passed as `config` to customize the lookup behavior. + /// Optional cache can be passed as `cache` to significantly speed up large + /// amounts of subsequent lookups. + /// + /// - Note: Even though cache can significantly speed up large amounts of subsequent lookups, + /// it shouldn't be used for one-off lookups as the initial cost of building up cache could be higher that the total time saved. /// /// - Returns: An array of `LookupResult` for `identifier` at this syntax node, /// ordered by visibility. If `identifier` is set to `nil`, returns all available names ordered by visibility. @@ -42,9 +47,60 @@ extension SyntaxProtocol { /// due to the ordering rules within the function body. public func lookup( _ identifier: Identifier?, - with config: LookupConfig = LookupConfig() + with config: LookupConfig = LookupConfig(), + cache: LookupCache? = nil ) -> [LookupResult] { - scope?.lookup(identifier, at: self.position, with: config) ?? [] + if let cache, let identifier { + let filteredResult: [LookupResult] = (scope?.lookup(nil, at: self.position, with: config, cache: cache) ?? []) + .compactMap { result in + switch result { + case .fromScope(let syntax, let withNames): + let filteredNames = + withNames + .filter { name in + name.identifier == identifier + } + + guard filteredNames.count != 0 else { return nil } + return .fromScope(syntax, withNames: filteredNames) + default: + return result + } + } + + var resultWithMergedSequentialResults: [LookupResult] = [] + var i = 0 + + while i < filteredResult.count { + let thisResult = filteredResult[i] + + if i < filteredResult.count - 1, + case .fromScope(let thisScope, let thisNames) = thisResult, + thisScope.asProtocol(SyntaxProtocol.self) is SequentialScopeSyntax + { + var accumulator = thisNames + + while i < filteredResult.count - 1, + case .fromScope(let otherScope, let otherNames) = filteredResult[i + 1], + otherScope.asProtocol(SyntaxProtocol.self) is SequentialScopeSyntax, + thisScope.id == otherScope.id + { + accumulator += otherNames + i += 1 + } + + resultWithMergedSequentialResults.append(.fromScope(thisScope, withNames: accumulator)) + } else { + resultWithMergedSequentialResults.append(thisResult) + } + + i += 1 + } + + return resultWithMergedSequentialResults + } else { + return scope?.lookup(identifier, at: self.position, with: config, cache: cache) ?? [] + } } } @@ -62,7 +118,8 @@ extension SyntaxProtocol { func lookup( _ identifier: Identifier?, at lookUpPosition: AbsolutePosition, - with config: LookupConfig + with config: LookupConfig, + cache: LookupCache? ) -> [LookupResult] } @@ -77,9 +134,10 @@ extension SyntaxProtocol { @_spi(Experimental) public func lookup( _ identifier: Identifier?, at lookUpPosition: AbsolutePosition, - with config: LookupConfig + with config: LookupConfig, + cache: LookupCache? ) -> [LookupResult] { - defaultLookupImplementation(identifier, at: lookUpPosition, with: config) + defaultLookupImplementation(identifier, at: lookUpPosition, with: config, cache: cache) } /// Returns `LookupResult` of all names introduced in this scope that `identifier` @@ -90,6 +148,7 @@ extension SyntaxProtocol { _ identifier: Identifier?, at lookUpPosition: AbsolutePosition, with config: LookupConfig, + cache: LookupCache?, propagateToParent: Bool = true ) -> [LookupResult] { let filteredNames = @@ -99,16 +158,31 @@ extension SyntaxProtocol { } return LookupResult.getResultArray(for: self, withNames: filteredNames) - + (propagateToParent ? lookupInParent(identifier, at: lookUpPosition, with: config) : []) + + (propagateToParent ? lookupInParent(identifier, at: lookUpPosition, with: config, cache: cache) : []) } /// Looks up in parent scope. func lookupInParent( _ identifier: Identifier?, at lookUpPosition: AbsolutePosition, - with config: LookupConfig + with config: LookupConfig, + cache: LookupCache? ) -> [LookupResult] { - parentScope?.lookup(identifier, at: lookUpPosition, with: config) ?? [] + guard !config.finishInSequentialScope else { + return parentScope?.lookup(identifier, at: lookUpPosition, with: config, cache: cache) ?? [] + } + + if let cachedAncestorResults = cache?.getCachedAncestorResults(id: id) { + return cachedAncestorResults + } + + let ancestorResults = parentScope?.lookup(identifier, at: lookUpPosition, with: config, cache: cache) ?? [] + + if let cache { + cache.setCachedAncestorResults(id: id, results: ancestorResults) + } + + return ancestorResults } func checkIdentifier( diff --git a/Sources/SwiftLexicalLookup/Scopes/SequentialScopeSyntax.swift b/Sources/SwiftLexicalLookup/Scopes/SequentialScopeSyntax.swift index e917b70cb97..b8a413600f7 100644 --- a/Sources/SwiftLexicalLookup/Scopes/SequentialScopeSyntax.swift +++ b/Sources/SwiftLexicalLookup/Scopes/SequentialScopeSyntax.swift @@ -43,6 +43,7 @@ extension SequentialScopeSyntax { _ identifier: Identifier?, at lookUpPosition: AbsolutePosition, with config: LookupConfig, + cache: LookupCache?, ignoreNamedDecl: Bool = false, propagateToParent: Bool = true ) -> [LookupResult] { @@ -62,69 +63,95 @@ extension SequentialScopeSyntax { // name matching on these and appends as a separate result to the results. var collectedNamedDecls: [NamedDeclSyntax] = [] - for codeBlockItem in codeBlockItems.reversed() { - if let namedDecl = codeBlockItem.item.asProtocol(NamedDeclSyntax.self) { - guard !ignoreNamedDecl else { continue } + if let cachedResults = cache?.getCachedSequentialResults(id: id) { + results = cachedResults + } else { + for codeBlockItem in codeBlockItems.reversed() { + if let namedDecl = codeBlockItem.item.asProtocol(NamedDeclSyntax.self) { + guard !ignoreNamedDecl else { continue } - collectedNamedDecls.append(namedDecl) - continue - } else if let ifConfigDecl = codeBlockItem.item.as(IfConfigDeclSyntax.self), - !ignoreNamedDecl - { - collectedNamedDecls += ifConfigDecl.getNamedDecls(for: config) - } + collectedNamedDecls.append(namedDecl) + continue + } else if let ifConfigDecl = codeBlockItem.item.as(IfConfigDeclSyntax.self), + !ignoreNamedDecl + { + collectedNamedDecls += ifConfigDecl.getNamedDecls(for: config) + } - if let introducingToParentScope = Syntax(codeBlockItem.item).asProtocol(SyntaxProtocol.self) - as? IntroducingToSequentialParentScopeSyntax - { - // Get results from encountered scope. - let introducedResults = introducingToParentScope.lookupFromSequentialParent( - identifier, - at: lookUpPosition, - with: config - ) + if let introducingToParentScope = Syntax(codeBlockItem.item).asProtocol(SyntaxProtocol.self) + as? IntroducingToSequentialParentScopeSyntax + { + // Get results from encountered scope. + let introducedResults = introducingToParentScope.lookupFromSequentialParent( + cache == nil ? identifier : nil, + at: cache == nil ? lookUpPosition : endPosition, + with: config, + cache: cache + ) - // Skip, if no results were found. - guard !introducedResults.isEmpty else { continue } + // Skip, if no results were found. + guard !introducedResults.isEmpty else { continue } - // If there are some names collected, create a new result for this scope. - if !currentChunk.isEmpty { - results.append(.fromScope(Syntax(self), withNames: currentChunk)) - currentChunk = [] - } + // If there are some names collected, create a new result for this scope. + if !currentChunk.isEmpty { + results.append(.fromScope(Syntax(self), withNames: currentChunk)) + currentChunk = [] + } - results += introducedResults - } else { - // Extract new names from encountered node. - currentChunk += LookupName.getNames( - from: codeBlockItem.item, - accessibleAfter: codeBlockItem.item.endPosition - ).filter { introducedName in - checkIdentifier(identifier, refersTo: introducedName, at: lookUpPosition) + results += introducedResults + } else { + // Extract new names from encountered node. + currentChunk += LookupName.getNames( + from: codeBlockItem.item, + accessibleAfter: codeBlockItem.item.endPosition + ) } } - } - // If there are some names collected, create a new result for this scope. - if !currentChunk.isEmpty { - results.append(.fromScope(Syntax(self), withNames: currentChunk)) - currentChunk = [] - } + // If there are some names collected, create a new result for this scope. + if !currentChunk.isEmpty { + results.append(.fromScope(Syntax(self), withNames: currentChunk)) + currentChunk = [] + } - // Filter named decls to be appended to the results. - for namedDecl in collectedNamedDecls.reversed() { - currentChunk += LookupName.getNames( - from: namedDecl, - accessibleAfter: namedDecl.endPosition - ).filter { introducedName in - checkIdentifier(identifier, refersTo: introducedName, at: lookUpPosition) + // Filter named decls to be appended to the results. + for namedDecl in collectedNamedDecls.reversed() { + currentChunk += LookupName.getNames( + from: namedDecl, + accessibleAfter: namedDecl.endPosition + ) } + + results += LookupResult.getResultArray(for: self, withNames: currentChunk) + + cache?.setCachedSequentialResults(id: id, results: results) } - results += LookupResult.getResultArray(for: self, withNames: currentChunk) + results = + results + .compactMap { result in + if case .fromScope(let scope, let cachedNames) = result { + if let guardStmt = scope.as(GuardStmtSyntax.self), guardStmt.body.range.contains(lookUpPosition) { + return nil // If lookup position is from guard body, ignore this result. + } + + let filteredNames = cachedNames.filter { + checkIdentifier(identifier, refersTo: $0, at: lookUpPosition) + } + + guard !filteredNames.isEmpty else { return nil } + + return .fromScope( + scope, + withNames: filteredNames + ) + } else { + return result + } + } return results + (config.finishInSequentialScope || !propagateToParent - ? [] : lookupInParent(identifier, at: lookUpPosition, with: config)) + ? [] : lookupInParent(identifier, at: lookUpPosition, with: config, cache: cache)) } } diff --git a/Sources/SwiftLexicalLookup/Scopes/WithGenericParametersScopeSyntax.swift b/Sources/SwiftLexicalLookup/Scopes/WithGenericParametersScopeSyntax.swift index 8bf75dfa824..55fbd892b0f 100644 --- a/Sources/SwiftLexicalLookup/Scopes/WithGenericParametersScopeSyntax.swift +++ b/Sources/SwiftLexicalLookup/Scopes/WithGenericParametersScopeSyntax.swift @@ -18,7 +18,8 @@ import SwiftSyntax func returningLookupFromGenericParameterScope( _ identifier: Identifier?, at lookUpPosition: AbsolutePosition, - with config: LookupConfig + with config: LookupConfig, + cache: LookupCache? ) -> [LookupResult] } @@ -42,18 +43,21 @@ import SwiftSyntax @_spi(Experimental) public func lookup( _ identifier: Identifier?, at lookUpPosition: AbsolutePosition, - with config: LookupConfig + with config: LookupConfig, + cache: LookupCache? ) -> [LookupResult] { return defaultLookupImplementation( identifier, at: position, with: config, + cache: cache, propagateToParent: false ) + lookupThroughGenericParameterScope( identifier, at: lookUpPosition, - with: config + with: config, + cache: cache ) } @@ -76,20 +80,22 @@ import SwiftSyntax @_spi(Experimental) public func lookupThroughGenericParameterScope( _ identifier: Identifier?, at lookUpPosition: AbsolutePosition, - with config: LookupConfig + with config: LookupConfig, + cache: LookupCache? ) -> [LookupResult] { if let genericParameterClause { - return genericParameterClause.lookup(identifier, at: lookUpPosition, with: config) + return genericParameterClause.lookup(identifier, at: lookUpPosition, with: config, cache: cache) } else { - return returningLookupFromGenericParameterScope(identifier, at: lookUpPosition, with: config) + return returningLookupFromGenericParameterScope(identifier, at: lookUpPosition, with: config, cache: cache) } } @_spi(Experimental) public func returningLookupFromGenericParameterScope( _ identifier: Identifier?, at lookUpPosition: AbsolutePosition, - with config: LookupConfig + with config: LookupConfig, + cache: LookupCache? ) -> [LookupResult] { - lookupInParent(identifier, at: lookUpPosition, with: config) + lookupInParent(identifier, at: lookUpPosition, with: config, cache: cache) } } diff --git a/Tests/SwiftLexicalLookupTest/Assertions.swift b/Tests/SwiftLexicalLookupTest/Assertions.swift index 6b64adc3a63..fc01756f258 100644 --- a/Tests/SwiftLexicalLookupTest/Assertions.swift +++ b/Tests/SwiftLexicalLookupTest/Assertions.swift @@ -90,37 +90,78 @@ func assertLexicalNameLookup( useNilAsTheParameter: Bool = false, config: LookupConfig = LookupConfig() ) { + let expectedResults = references.mapValues { expectations in + expectations.flatMap { expectation in + expectation.expectedNames.flatMap { expectedName in + expectedName.marker + } + } + } + + // Perform test without cache + assertLexicalScopeQuery( + source: source, + methodUnderTest: { marker, tokenAtMarker in + testFunction( + marker: marker, + tokenAtMarker: tokenAtMarker, + references: references, + useNilAsTheParameter: useNilAsTheParameter, + config: config, + cache: nil + ) + }, + expected: expectedResults, + expectedResultTypes: expectedResultTypes + ) + + // Perform test with cache + let cache = LookupCache() assertLexicalScopeQuery( source: source, methodUnderTest: { marker, tokenAtMarker in - let lookupIdentifier = Identifier(tokenAtMarker) + testFunction( + marker: marker, + tokenAtMarker: tokenAtMarker, + references: references, + useNilAsTheParameter: useNilAsTheParameter, + config: config, + cache: cache + ) + }, + expected: expectedResults, + expectedResultTypes: expectedResultTypes + ) +} - let result = tokenAtMarker.lookup(useNilAsTheParameter ? nil : lookupIdentifier, with: config) +/// Asserts result of unqualified lookup for the given `marker` and `tokenAtMarker`. +/// Returns flattened array of syntax nodes returned by the query. +private func testFunction( + marker: String, + tokenAtMarker: TokenSyntax, + references: [String: [ResultExpectation]], + useNilAsTheParameter: Bool, + config: LookupConfig, + cache: LookupCache? +) -> [SyntaxProtocol] { + let lookupIdentifier = Identifier(tokenAtMarker) - guard let expectedValues = references[marker] else { - XCTFail("For marker \(marker), couldn't find result expectation") - return [] - } + let result = tokenAtMarker.lookup(useNilAsTheParameter ? nil : lookupIdentifier, with: config, cache: cache) - ResultExpectation.assertResult(marker: marker, result: result, expectedValues: expectedValues) + guard let expectedValues = references[marker] else { + XCTFail("For marker \(marker), couldn't find result expectation") + return [] + } - return result.flatMap { lookUpResult in - lookUpResult.names.flatMap { lookupName in - if case .equivalentNames(let names) = lookupName { - return names.map(\.syntax) - } else { - return [lookupName.syntax] - } - } - } - }, - expected: references.mapValues { expectations in - expectations.flatMap { expectation in - expectation.expectedNames.flatMap { expectedName in - expectedName.marker - } + ResultExpectation.assertResult(marker: marker, result: result, expectedValues: expectedValues) + + return result.flatMap { lookUpResult in + lookUpResult.names.flatMap { lookupName in + if case .equivalentNames(let names) = lookupName { + return names.map(\.syntax) + } else { + return [lookupName.syntax] } - }, - expectedResultTypes: expectedResultTypes - ) + } + } }