Skip to content

Commit e7c02d8

Browse files
committed
Add SwiftLexicalLookup result caching.
1 parent e547074 commit e7c02d8

11 files changed

+360
-121
lines changed

Sources/SwiftLexicalLookup/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
add_swift_syntax_library(SwiftLexicalLookup
1010
IdentifiableSyntax.swift
11+
LookupCache.swift
1112
LookupName.swift
1213
LookupResult.swift
1314
SimpleLookupQueries.swift
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import SwiftSyntax
14+
15+
/// Unqualified lookup cache. Should be used when performing
16+
/// large sequences of adjacent lookups to maximise performance.
17+
public class LookupCache {
18+
/// Cached results of `ScopeSyntax.lookupParent` calls.
19+
/// Identified by `SyntaxIdentifier`.
20+
private var ancestorResultsCache: [SyntaxIdentifier: [LookupResult]] = [:]
21+
/// Cached results of `SequentialScopeSyntax.sequentialLookup` calls.
22+
/// Identified by `SyntaxIdentifier`.
23+
private var sequentialResultsCache: [SyntaxIdentifier: [LookupResult]] = [:]
24+
/// Looked-up scope identifiers during cache accesses.
25+
private var hits: Set<SyntaxIdentifier> = []
26+
27+
private let dropMod: Int
28+
private var evictionCount = 0
29+
30+
/// Creates a new unqualified lookup cache.
31+
/// `drop` parameter specifies how many eviction calls will be
32+
/// ignored before evicting not-hit members of the cache.
33+
///
34+
/// Example cache eviction sequences (s - skip, e - evict):
35+
/// - `drop = 0` - `e -> e -> e -> e -> e -> ...`
36+
/// - `drop = 1` - `s -> e -> s -> s -> e -> ...`
37+
/// - `drop = 3` - `s -> s -> s -> e -> s -> ...`
38+
///
39+
/// - Note: `drop = 0` effectively maintains exactly one path of cached results to
40+
/// the root in the cache (assuming we evict cache members after each lookup in a sequence of lookups).
41+
/// Higher the `drop` value, more such paths can potentially be stored in the cache at any given moment.
42+
/// Because of that, a higher `drop` value also translates to a higher number of cache-hits,
43+
/// but it might not directly translate to better performance. Because of a larger memory footprint,
44+
/// memory accesses could take longer, slowing down the eviction process. That's why the `drop` value
45+
/// could be fine-tuned to maximize the performance given file size,
46+
/// number of lookups, and amount of available memory.
47+
public init(drop: Int = 0) {
48+
self.dropMod = drop + 1
49+
}
50+
51+
/// Get cached ancestor results for the given `id`.
52+
/// `nil` if there's no cache entry for the given `id`.
53+
/// Adds `id` and ids of all ancestors to the cache `hits`.
54+
func getCachedAncestorResults(id: SyntaxIdentifier) -> [LookupResult]? {
55+
guard let results = ancestorResultsCache[id] else { return nil }
56+
hits.formUnion(results.map(\.scope.id))
57+
hits.insert(id)
58+
return results
59+
}
60+
61+
/// Set cached ancestor results for the given `id`.
62+
/// Adds `id` to the cache `hits`.
63+
func setCachedAncestorResults(id: SyntaxIdentifier, results: [LookupResult]) {
64+
hits.insert(id)
65+
ancestorResultsCache[id] = results
66+
}
67+
68+
/// Get cached sequential lookup results for the given `id`.
69+
/// `nil` if there's no cache entry for the given `id`.
70+
/// Adds `id` to the cache `hits`.
71+
func getCachedSequentialResults(id: SyntaxIdentifier) -> [LookupResult]? {
72+
guard let results = sequentialResultsCache[id] else { return nil }
73+
hits.insert(id)
74+
return results
75+
}
76+
77+
/// Set cached sequential lookup results for the given `id`.
78+
/// Adds `id` to the cache `hits`.
79+
func setCachedSequentialResults(id: SyntaxIdentifier, results: [LookupResult]) {
80+
hits.insert(id)
81+
sequentialResultsCache[id] = results
82+
}
83+
84+
/// Removes all cached entries without a hit, unless it's prohibited
85+
/// by the internal drop counter (as specified by `drop` in the initializer).
86+
/// The dropping behavior can be disabled for this call with the `bypassDropCounter`
87+
/// parameter, resulting in immediate eviction of entries without a hit.
88+
public func evictEntriesWithoutHit(bypassDropCounter: Bool = false) {
89+
if !bypassDropCounter {
90+
evictionCount = (evictionCount + 1) % dropMod
91+
guard evictionCount != 0 else { return }
92+
}
93+
94+
for key in Set(ancestorResultsCache.keys).union(sequentialResultsCache.keys).subtracting(hits) {
95+
ancestorResultsCache.removeValue(forKey: key)
96+
sequentialResultsCache.removeValue(forKey: key)
97+
}
98+
99+
hits = []
100+
}
101+
}

Sources/SwiftLexicalLookup/Scopes/CanInterleaveResultsLaterScopeSyntax.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ protocol CanInterleaveResultsLaterScopeSyntax: ScopeSyntax {
2020
_ identifier: Identifier?,
2121
at lookUpPosition: AbsolutePosition,
2222
with config: LookupConfig,
23+
cache: LookupCache?,
2324
resultsToInterleave: [LookupResult]
2425
) -> [LookupResult]
2526
}

Sources/SwiftLexicalLookup/Scopes/FunctionScopeSyntax.swift

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ extension FunctionScopeSyntax {
3030
@_spi(Experimental) public func lookup(
3131
_ identifier: Identifier?,
3232
at lookUpPosition: AbsolutePosition,
33-
with config: LookupConfig
33+
with config: LookupConfig,
34+
cache: LookupCache?
3435
) -> [LookupResult] {
3536
var thisScopeResults: [LookupResult] = []
3637

@@ -39,6 +40,7 @@ extension FunctionScopeSyntax {
3940
identifier,
4041
at: position,
4142
with: config,
43+
cache: cache,
4244
propagateToParent: false
4345
)
4446
}
@@ -47,7 +49,8 @@ extension FunctionScopeSyntax {
4749
+ lookupThroughGenericParameterScope(
4850
identifier,
4951
at: lookUpPosition,
50-
with: config
52+
with: config,
53+
cache: cache
5154
)
5255
}
5356
}

Sources/SwiftLexicalLookup/Scopes/GenericParameterScopeSyntax.swift

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,18 +40,21 @@ protocol GenericParameterScopeSyntax: ScopeSyntax {}
4040
@_spi(Experimental) public func lookup(
4141
_ identifier: Identifier?,
4242
at lookUpPosition: AbsolutePosition,
43-
with config: LookupConfig
43+
with config: LookupConfig,
44+
cache: LookupCache?
4445
) -> [LookupResult] {
4546
return defaultLookupImplementation(
4647
identifier,
4748
at: lookUpPosition,
4849
with: config,
50+
cache: cache,
4951
propagateToParent: false
5052
)
5153
+ lookupBypassingParentResults(
5254
identifier,
5355
at: lookUpPosition,
54-
with: config
56+
with: config,
57+
cache: cache
5558
)
5659
}
5760

@@ -76,16 +79,22 @@ protocol GenericParameterScopeSyntax: ScopeSyntax {}
7679
private func lookupBypassingParentResults(
7780
_ identifier: Identifier?,
7881
at lookUpPosition: AbsolutePosition,
79-
with config: LookupConfig
82+
with config: LookupConfig,
83+
cache: LookupCache?
8084
) -> [LookupResult] {
8185
guard let parentScope else { return [] }
8286

8387
if let parentScope = Syntax(parentScope).asProtocol(SyntaxProtocol.self)
8488
as? WithGenericParametersScopeSyntax
8589
{
86-
return parentScope.returningLookupFromGenericParameterScope(identifier, at: lookUpPosition, with: config)
90+
return parentScope.returningLookupFromGenericParameterScope(
91+
identifier,
92+
at: lookUpPosition,
93+
with: config,
94+
cache: cache
95+
)
8796
} else {
88-
return lookupInParent(identifier, at: lookUpPosition, with: config)
97+
return lookupInParent(identifier, at: lookUpPosition, with: config, cache: cache)
8998
}
9099
}
91100
}

Sources/SwiftLexicalLookup/Scopes/IntroducingToSequentialParentScopeSyntax.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ protocol IntroducingToSequentialParentScopeSyntax: ScopeSyntax {
2121
func lookupFromSequentialParent(
2222
_ identifier: Identifier?,
2323
at lookUpPosition: AbsolutePosition,
24-
with config: LookupConfig
24+
with config: LookupConfig,
25+
cache: LookupCache?
2526
) -> [LookupResult]
2627
}

Sources/SwiftLexicalLookup/Scopes/NominalTypeDeclSyntax.swift

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,16 +34,17 @@ extension NominalTypeDeclSyntax {
3434
@_spi(Experimental) public func returningLookupFromGenericParameterScope(
3535
_ identifier: Identifier?,
3636
at lookUpPosition: AbsolutePosition,
37-
with config: LookupConfig
37+
with config: LookupConfig,
38+
cache: LookupCache?
3839
) -> [LookupResult] {
3940
if let inheritanceClause, inheritanceClause.range.contains(lookUpPosition) {
40-
return lookupInParent(identifier, at: lookUpPosition, with: config)
41+
return lookupInParent(identifier, at: lookUpPosition, with: config, cache: cache)
4142
} else if let genericParameterClause, genericParameterClause.range.contains(lookUpPosition) {
42-
return lookupInParent(identifier, at: lookUpPosition, with: config)
43+
return lookupInParent(identifier, at: lookUpPosition, with: config, cache: cache)
4344
} else if name.range.contains(lookUpPosition) || genericWhereClause?.range.contains(lookUpPosition) ?? false {
44-
return lookupInParent(identifier, at: lookUpPosition, with: config)
45+
return lookupInParent(identifier, at: lookUpPosition, with: config, cache: cache)
4546
} else {
46-
return [.lookInMembers(Syntax(self))] + lookupInParent(identifier, at: lookUpPosition, with: config)
47+
return [.lookInMembers(Syntax(self))] + lookupInParent(identifier, at: lookUpPosition, with: config, cache: cache)
4748
}
4849
}
4950
}

0 commit comments

Comments
 (0)