Skip to content

Commit b514509

Browse files
authored
Merge pull request #2682 from rintaro/macros-parsed-syntax-registry
[Macros] Cache parsed syntax tree in compiler plugins
2 parents 52f0fb1 + baff373 commit b514509

File tree

6 files changed

+211
-18
lines changed

6 files changed

+211
-18
lines changed

Sources/SwiftCompilerPluginMessageHandling/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
add_swift_syntax_library(SwiftCompilerPluginMessageHandling
1010
CompilerPluginMessageHandler.swift
1111
Diagnostics.swift
12+
LRUCache.swift
1213
Macros.swift
1314
PluginMacroExpansionContext.swift
1415
PluginMessageCompatibility.swift

Sources/SwiftCompilerPluginMessageHandling/CompilerPluginMessageHandler.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,11 +112,15 @@ public class CompilerPluginMessageHandler<Provider: PluginProvider> {
112112
/// Object to provide actual plugin functions.
113113
let provider: Provider
114114

115+
/// Syntax registry shared between multiple requests.
116+
let syntaxRegistry: ParsedSyntaxRegistry
117+
115118
/// Plugin host capability
116119
var hostCapability: HostCapability
117120

118121
public init(provider: Provider) {
119122
self.provider = provider
123+
self.syntaxRegistry = ParsedSyntaxRegistry(cacheCapacity: 16)
120124
self.hostCapability = HostCapability()
121125
}
122126

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift open source project
4+
//
5+
// Copyright (c) 2024 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See http://swift.org/LICENSE.txt for license information
9+
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
/// Simple LRU cache.
14+
@_spi(Testing)
15+
public class LRUCache<Key: Hashable, Value> {
16+
private class _Node {
17+
unowned var prev: _Node? = nil
18+
unowned var next: _Node? = nil
19+
20+
let key: Key
21+
var value: Value
22+
23+
init(key: Key, value: Value) {
24+
self.key = key
25+
self.value = value
26+
}
27+
}
28+
29+
private var table: [Key: _Node]
30+
31+
// Double linked list
32+
private unowned var head: _Node?
33+
private unowned var tail: _Node?
34+
35+
public let capacity: Int
36+
37+
public init(capacity: Int) {
38+
self.table = [:]
39+
self.head = nil
40+
self.tail = nil
41+
self.capacity = capacity
42+
}
43+
44+
public var count: Int {
45+
return table.count
46+
}
47+
48+
public subscript(key: Key) -> Value? {
49+
get {
50+
guard let node = table[key] else {
51+
return nil
52+
}
53+
moveToHead(node: node)
54+
return node.value
55+
}
56+
57+
set {
58+
switch (table[key], newValue) {
59+
case let (nil, newValue?): // create.
60+
self.ensureCapacityForNewValue()
61+
let node = _Node(key: key, value: newValue)
62+
addToHead(node: node)
63+
table[key] = node
64+
65+
case let (node?, newValue?): // update.
66+
moveToHead(node: node)
67+
node.value = newValue
68+
69+
case let (node?, nil): // delete.
70+
remove(node: node)
71+
table[key] = nil
72+
73+
case (nil, nil): // no-op.
74+
break
75+
}
76+
}
77+
}
78+
79+
private func ensureCapacityForNewValue() {
80+
while self.table.count >= self.capacity, let tail = self.tail {
81+
remove(node: tail)
82+
table[tail.key] = nil
83+
}
84+
}
85+
86+
private func moveToHead(node: _Node) {
87+
if node === self.head {
88+
return
89+
}
90+
remove(node: node)
91+
addToHead(node: node)
92+
}
93+
94+
private func addToHead(node: _Node) {
95+
node.next = self.head
96+
node.next?.prev = node
97+
node.prev = nil
98+
self.head = node
99+
if self.tail == nil {
100+
self.tail = node
101+
}
102+
}
103+
104+
private func remove(node: _Node) {
105+
node.next?.prev = node.prev
106+
node.prev?.next = node.next
107+
if node === self.head {
108+
self.head = node.next
109+
}
110+
if node === self.tail {
111+
self.tail = node.prev
112+
}
113+
node.prev = nil
114+
node.next = nil
115+
}
116+
}

Sources/SwiftCompilerPluginMessageHandling/Macros.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ extension CompilerPluginMessageHandler {
5656
expandingSyntax: PluginMessage.Syntax,
5757
lexicalContext: [PluginMessage.Syntax]?
5858
) -> PluginToHostMessage {
59-
let sourceManager = SourceManager()
59+
let sourceManager = SourceManager(syntaxRegistry: syntaxRegistry)
6060
let syntax = sourceManager.add(expandingSyntax, foldingWith: .standardOperators)
6161

6262
let context = PluginMacroExpansionContext(
@@ -120,7 +120,7 @@ extension CompilerPluginMessageHandler {
120120
conformanceListSyntax: PluginMessage.Syntax?,
121121
lexicalContext: [PluginMessage.Syntax]?
122122
) -> PluginToHostMessage {
123-
let sourceManager = SourceManager()
123+
let sourceManager = SourceManager(syntaxRegistry: syntaxRegistry)
124124
let attributeNode = sourceManager.add(
125125
attributeSyntax,
126126
foldingWith: .standardOperators

Sources/SwiftCompilerPluginMessageHandling/PluginMacroExpansionContext.swift

Lines changed: 51 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,49 @@ import SwiftSyntax
2424
import SwiftSyntaxMacros
2525
#endif
2626

27+
/// Caching parser for PluginMessage.Syntax
28+
class ParsedSyntaxRegistry {
29+
struct Key: Hashable {
30+
let source: String
31+
let kind: PluginMessage.Syntax.Kind
32+
}
33+
34+
private var storage: LRUCache<Key, Syntax>
35+
36+
init(cacheCapacity: Int) {
37+
self.storage = LRUCache(capacity: cacheCapacity)
38+
}
39+
40+
private func parse(source: String, kind: PluginMessage.Syntax.Kind) -> Syntax {
41+
var parser = Parser(source)
42+
switch kind {
43+
case .declaration:
44+
return Syntax(DeclSyntax.parse(from: &parser))
45+
case .statement:
46+
return Syntax(StmtSyntax.parse(from: &parser))
47+
case .expression:
48+
return Syntax(ExprSyntax.parse(from: &parser))
49+
case .type:
50+
return Syntax(TypeSyntax.parse(from: &parser))
51+
case .pattern:
52+
return Syntax(PatternSyntax.parse(from: &parser))
53+
case .attribute:
54+
return Syntax(AttributeSyntax.parse(from: &parser))
55+
}
56+
}
57+
58+
func get(source: String, kind: PluginMessage.Syntax.Kind) -> Syntax {
59+
let key = Key(source: source, kind: kind)
60+
if let cached = storage[key] {
61+
return cached
62+
}
63+
64+
let node = parse(source: source, kind: kind)
65+
storage[key] = node
66+
return node
67+
}
68+
}
69+
2770
/// Manages known source code combined with their filename/fileID. This can be
2871
/// used to get line/column from a syntax node in the managed source code.
2972
class SourceManager {
@@ -67,32 +110,24 @@ class SourceManager {
67110
var endUTF8Offset: Int
68111
}
69112

113+
/// Caching syntax parser.
114+
private let syntaxRegistry: ParsedSyntaxRegistry
115+
70116
/// Syntax added by `add(_:)` method. Keyed by the `id` of the node.
71117
private var knownSourceSyntax: [Syntax.ID: KnownSourceSyntax] = [:]
72118

119+
init(syntaxRegistry: ParsedSyntaxRegistry) {
120+
self.syntaxRegistry = syntaxRegistry
121+
}
122+
73123
/// Convert syntax information to a ``Syntax`` node. The location informations
74124
/// are cached in the source manager to provide `location(of:)` et al.
75125
func add(
76126
_ syntaxInfo: PluginMessage.Syntax,
77127
foldingWith operatorTable: OperatorTable? = nil
78128
) -> Syntax {
79129

80-
var node: Syntax
81-
var parser = Parser(syntaxInfo.source)
82-
switch syntaxInfo.kind {
83-
case .declaration:
84-
node = Syntax(DeclSyntax.parse(from: &parser))
85-
case .statement:
86-
node = Syntax(StmtSyntax.parse(from: &parser))
87-
case .expression:
88-
node = Syntax(ExprSyntax.parse(from: &parser))
89-
case .type:
90-
node = Syntax(TypeSyntax.parse(from: &parser))
91-
case .pattern:
92-
node = Syntax(PatternSyntax.parse(from: &parser))
93-
case .attribute:
94-
node = Syntax(AttributeSyntax.parse(from: &parser))
95-
}
130+
var node = syntaxRegistry.get(source: syntaxInfo.source, kind: syntaxInfo.kind)
96131
if let operatorTable {
97132
node = operatorTable.foldAll(node, errorHandler: { _ in /*ignore*/ })
98133
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 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+
@_spi(Testing) import SwiftCompilerPluginMessageHandling
14+
import XCTest
15+
16+
final class LRUCacheTests: XCTestCase {
17+
func testBasic() {
18+
let cache = LRUCache<String, Int>(capacity: 2)
19+
cache["foo"] = 0
20+
cache["bar"] = 1
21+
XCTAssertEqual(cache["foo"], 0)
22+
cache["baz"] = 2
23+
XCTAssertEqual(cache["foo"], 0)
24+
XCTAssertEqual(cache["bar"], nil)
25+
XCTAssertEqual(cache["baz"], 2)
26+
XCTAssertEqual(cache.count, 2)
27+
28+
cache["qux"] = nil
29+
cache["baz"] = nil
30+
cache["foo"] = 10
31+
XCTAssertEqual(cache["foo"], 10)
32+
XCTAssertEqual(cache["bar"], nil)
33+
XCTAssertEqual(cache["baz"], nil)
34+
XCTAssertEqual(cache["qux"], nil)
35+
XCTAssertEqual(cache.count, 1)
36+
}
37+
}

0 commit comments

Comments
 (0)