Skip to content

Commit 6be8e8d

Browse files
committed
[SyntaxRewriter] Optimize SyntaxRewriter visitation
Similar treatment as 'SyntaxVisitor'. Reuse `Syntax.Info` when it's safe to do so (i.e. uniquely referenced)
1 parent 70e3741 commit 6be8e8d

File tree

2 files changed

+718
-632
lines changed

2 files changed

+718
-632
lines changed

CodeGeneration/Sources/generate-swift-syntax/templates/swiftsyntax/SyntaxRewriterFile.swift

Lines changed: 73 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,31 @@ let syntaxRewriterFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
4242
"""
4343
)
4444

45+
DeclSyntax(
46+
"""
47+
/// `Syntax.Info` salvaged from the node being deinitialized in 'visitChildren'.
48+
///
49+
/// Instead of deallocating them and allocating memory for new syntax nodes, store the allocated memory in an array.
50+
/// We can then re-use them to create new syntax nodes.
51+
///
52+
/// The array's size should be a typical nesting depth of a Swift file. That way we can store all allocated syntax
53+
/// nodes when unwinding the visitation stack.
54+
///
55+
/// The actual `info` stored in the `Syntax.Info` objects is garbage. It needs to be set when any of the `Syntax.Info`
56+
/// objects get re-used.
57+
///
58+
/// Note: making the element non-nil causes 'swift::runtime::SwiftTLSContext::get()' traffic somehow.
59+
private var recyclableNodeInfos: ContiguousArray<Syntax.Info?>
60+
"""
61+
)
62+
4563
DeclSyntax(
4664
"""
4765
public init(viewMode: SyntaxTreeViewMode = .sourceAccurate) {
4866
self.viewMode = viewMode
4967
self.arena = nil
68+
self.recyclableNodeInfos = []
69+
self.recyclableNodeInfos.reserveCapacity(64)
5070
}
5171
"""
5272
)
@@ -57,6 +77,8 @@ let syntaxRewriterFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
5777
public init(viewMode: SyntaxTreeViewMode = .sourceAccurate, arena: SyntaxArena? = nil) {
5878
self.viewMode = viewMode
5979
self.arena = arena
80+
self.recyclableNodeInfos = []
81+
self.recyclableNodeInfos.reserveCapacity(64)
6082
}
6183
"""
6284
)
@@ -65,7 +87,8 @@ let syntaxRewriterFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
6587
"""
6688
/// Rewrite `node`, keeping its parent unless `detach` is `true`.
6789
public func rewrite(_ node: some SyntaxProtocol, detach: Bool = false) -> Syntax {
68-
let rewritten = self.dispatchVisit(Syntax(node))
90+
var rewritten = Syntax(node)
91+
self.dispatchVisit(&rewritten)
6992
if detach {
7093
return rewritten
7194
}
@@ -126,15 +149,19 @@ let syntaxRewriterFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
126149
/// - Returns: the rewritten node
127150
@available(*, deprecated, renamed: "rewrite(_:detach:)")
128151
public func visit(_ node: Syntax) -> Syntax {
129-
return dispatchVisit(node)
152+
var rewritten = node
153+
dispatchVisit(&rewritten)
154+
return rewritten
130155
}
131156
"""
132157
)
133158

134159
DeclSyntax(
135160
"""
136161
public func visit<T: SyntaxChildChoices>(_ node: T) -> T {
137-
return dispatchVisit(Syntax(node)).cast(T.self)
162+
var rewritten = Syntax(node)
163+
dispatchVisit(&rewritten)
164+
return rewritten.cast(T.self)
138165
}
139166
"""
140167
)
@@ -177,7 +204,9 @@ let syntaxRewriterFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
177204
/// - Returns: the rewritten node
178205
\(baseNode.apiAttributes())\
179206
public func visit(_ node: \(baseKind.syntaxType)) -> \(baseKind.syntaxType) {
180-
return dispatchVisit(Syntax(node)).cast(\(baseKind.syntaxType).self)
207+
var node: Syntax = Syntax(node)
208+
dispatchVisit(&node)
209+
return node.cast(\(baseKind.syntaxType).self)
181210
}
182211
"""
183212
)
@@ -187,21 +216,16 @@ let syntaxRewriterFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
187216
"""
188217
/// Interpret `node` as a node of type `nodeType`, visit it, calling
189218
/// the `visit` to transform the node.
219+
@inline(__always)
190220
private func visitImpl<NodeType: SyntaxProtocol>(
191-
_ node: Syntax,
221+
_ node: inout Syntax,
192222
_ nodeType: NodeType.Type,
193223
_ visit: (NodeType) -> some SyntaxProtocol
194-
) -> Syntax {
195-
let castedNode = node.cast(NodeType.self)
196-
// Accessing _syntaxNode directly is faster than calling Syntax(node)
197-
visitPre(node)
198-
defer {
199-
visitPost(node)
200-
}
201-
if let newNode = visitAny(node) {
202-
return newNode
203-
}
204-
return Syntax(visit(castedNode))
224+
) {
225+
let origNode = node
226+
visitPre(origNode)
227+
node = visitAny(origNode) ?? Syntax(visit(origNode.cast(NodeType.self)))
228+
visitPost(origNode)
205229
}
206230
"""
207231
)
@@ -242,26 +266,26 @@ let syntaxRewriterFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
242266
/// that determines the correct visitation function will be popped of the
243267
/// stack before the function is being called, making the switch's stack
244268
/// space transient instead of having it linger in the call stack.
245-
private func visitationFunc(for node: Syntax) -> ((Syntax) -> Syntax)
269+
private func visitationFunc(for node: Syntax) -> ((inout Syntax) -> Void)
246270
"""
247271
) {
248272
try SwitchExprSyntax("switch node.raw.kind") {
249273
SwitchCaseSyntax("case .token:") {
250-
StmtSyntax("return { self.visitImpl($0, TokenSyntax.self, self.visit) }")
274+
StmtSyntax("return { self.visitImpl(&$0, TokenSyntax.self, self.visit) }")
251275
}
252276

253277
for node in NON_BASE_SYNTAX_NODES {
254278
SwitchCaseSyntax("case .\(node.varOrCaseName):") {
255-
StmtSyntax("return { self.visitImpl($0, \(node.kind.syntaxType).self, self.visit) }")
279+
StmtSyntax("return { self.visitImpl(&$0, \(node.kind.syntaxType).self, self.visit) }")
256280
}
257281
}
258282
}
259283
}
260284

261285
DeclSyntax(
262286
"""
263-
private func dispatchVisit(_ node: Syntax) -> Syntax {
264-
return visitationFunc(for: node)(node)
287+
private func dispatchVisit(_ node: inout Syntax) {
288+
visitationFunc(for: node)(&node)
265289
}
266290
"""
267291
)
@@ -272,15 +296,15 @@ let syntaxRewriterFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
272296
poundKeyword: .poundElseToken(),
273297
elements: .statements(
274298
CodeBlockItemListSyntax {
275-
try! FunctionDeclSyntax("private func dispatchVisit(_ node: Syntax) -> Syntax") {
299+
try! FunctionDeclSyntax("private func dispatchVisit(_ node: inout Syntax)") {
276300
try SwitchExprSyntax("switch node.raw.kind") {
277301
SwitchCaseSyntax("case .token:") {
278-
StmtSyntax("return visitImpl(node, TokenSyntax.self, visit)")
302+
StmtSyntax("return visitImpl(&node, TokenSyntax.self, visit)")
279303
}
280304

281305
for node in NON_BASE_SYNTAX_NODES {
282306
SwitchCaseSyntax("case .\(node.varOrCaseName):") {
283-
StmtSyntax("return visitImpl(node, \(node.kind.syntaxType).self, visit)")
307+
StmtSyntax("return visitImpl(&node, \(node.kind.syntaxType).self, visit)")
284308
}
285309
}
286310
}
@@ -307,9 +331,9 @@ let syntaxRewriterFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
307331
// nodes are being collected.
308332
var newLayout: ContiguousArray<RawSyntax?>?
309333
310-
// Rewritten children just to keep their 'SyntaxArena' alive until they are
311-
// wrapped with 'Syntax'
312-
var rewrittens: ContiguousArray<Syntax> = []
334+
// Keep 'SyntaxArena' of rewritten nodes alive until they are wrapped
335+
// with 'Syntax'
336+
var rewrittens: ContiguousArray<RetainedSyntaxArena> = []
313337
314338
let syntaxNode = node._syntaxNode
315339
@@ -329,10 +353,18 @@ let syntaxRewriterFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
329353
}
330354
331355
// Build the Syntax node to rewrite
332-
let absoluteRaw = AbsoluteRawSyntax(raw: child, info: info)
356+
var childNode: Syntax
357+
if !recyclableNodeInfos.isEmpty {
358+
let recycledInfo: Syntax.Info = recyclableNodeInfos.removeLast()!
359+
recycledInfo.info = .nonRoot(.init(parent: Syntax(node), absoluteInfo: info))
360+
childNode = Syntax(child, info: recycledInfo)
361+
} else {
362+
let absoluteRaw = AbsoluteRawSyntax(raw: child, info: info)
363+
childNode = Syntax(absoluteRaw, parent: syntaxNode)
364+
}
333365
334-
let rewritten = dispatchVisit(Syntax(absoluteRaw, parent: syntaxNode))
335-
if rewritten.id != info.nodeId {
366+
dispatchVisit(&childNode)
367+
if childNode.raw.id != child.id {
336368
// The node was rewritten, let's handle it
337369
if newLayout == nil {
338370
// We have not yet collected any previous rewritten nodes. Initialize
@@ -350,15 +382,24 @@ let syntaxRewriterFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
350382
351383
// Now that we know we have a new layout in which we collect rewritten
352384
// nodes, add it.
353-
rewrittens.append(rewritten)
354-
newLayout!.append(rewritten.raw)
385+
rewrittens.append(childNode.raw.arenaReference.retained)
386+
newLayout!.append(childNode.raw)
355387
} else {
356388
// The node was not changed by the rewriter. Only store it if a previous
357389
// node has been rewritten and we are collecting a rewritten layout.
358390
if newLayout != nil {
359391
newLayout!.append(raw)
360392
}
361393
}
394+
395+
if recyclableNodeInfos.capacity > recyclableNodeInfos.count,
396+
isKnownUniquelyReferenced(&childNode.info) {
397+
var info: Syntax.Info! = nil
398+
// 'swap' to avoid ref-counting traffic.
399+
swap(&childNode.info, &info)
400+
info.info = nil
401+
recyclableNodeInfos.append(info)
402+
}
362403
}
363404
364405
if let newLayout {

0 commit comments

Comments
 (0)