Skip to content

Commit 3704707

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 9cc77ac commit 3704707

File tree

2 files changed

+714
-632
lines changed

2 files changed

+714
-632
lines changed

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

Lines changed: 69 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,20 @@ let syntaxRewriterFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
3939
/// tree, all of the rewritten node's parents also need to be re-created. This is the arena in which those
4040
/// intermediate nodes should be allocated.
4141
private let arena: SyntaxArena?
42+
43+
/// `Syntax.Info` salvaged from the node being deinitialized in 'visitChildren'.
44+
///
45+
/// Instead of deallocating them and allocating memory for new syntax nodes, store the allocated memory in an array.
46+
/// We can then re-use them to create new syntax nodes.
47+
///
48+
/// The array's size should be a typical nesting depth of a Swift file. That way we can store all allocated syntax
49+
/// nodes when unwinding the visitation stack.
50+
///
51+
/// The actual `info` stored in the `Syntax.Info` objects is garbage. It needs to be set when any of the `Syntax.Info`
52+
/// objects get re-used.
53+
///
54+
/// Note: making the element non-nil causes 'swift::runtime::SwiftTLSContext::get()' traffic somehow.
55+
private var recyclableNodeInfos: ContiguousArray<Syntax.Info?>
4256
"""
4357
)
4458

@@ -47,6 +61,8 @@ let syntaxRewriterFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
4761
public init(viewMode: SyntaxTreeViewMode = .sourceAccurate) {
4862
self.viewMode = viewMode
4963
self.arena = nil
64+
self.recyclableNodeInfos = []
65+
self.recyclableNodeInfos.reserveCapacity(64)
5066
}
5167
"""
5268
)
@@ -57,6 +73,8 @@ let syntaxRewriterFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
5773
public init(viewMode: SyntaxTreeViewMode = .sourceAccurate, arena: SyntaxArena? = nil) {
5874
self.viewMode = viewMode
5975
self.arena = arena
76+
self.recyclableNodeInfos = []
77+
self.recyclableNodeInfos.reserveCapacity(64)
6078
}
6179
"""
6280
)
@@ -65,7 +83,8 @@ let syntaxRewriterFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
6583
"""
6684
/// Rewrite `node`, keeping its parent unless `detach` is `true`.
6785
public func rewrite(_ node: some SyntaxProtocol, detach: Bool = false) -> Syntax {
68-
let rewritten = self.dispatchVisit(Syntax(node))
86+
var rewritten = Syntax(node)
87+
self.dispatchVisit(&rewritten)
6988
if detach {
7089
return rewritten
7190
}
@@ -126,15 +145,19 @@ let syntaxRewriterFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
126145
/// - Returns: the rewritten node
127146
@available(*, deprecated, renamed: "rewrite(_:detach:)")
128147
public func visit(_ node: Syntax) -> Syntax {
129-
return dispatchVisit(node)
148+
var rewritten = node
149+
dispatchVisit(&rewritten)
150+
return rewritten
130151
}
131152
"""
132153
)
133154

134155
DeclSyntax(
135156
"""
136157
public func visit<T: SyntaxChildChoices>(_ node: T) -> T {
137-
return dispatchVisit(Syntax(node)).cast(T.self)
158+
var rewritten = Syntax(node)
159+
dispatchVisit(&rewritten)
160+
return rewritten.cast(T.self)
138161
}
139162
"""
140163
)
@@ -177,7 +200,9 @@ let syntaxRewriterFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
177200
/// - Returns: the rewritten node
178201
\(baseNode.apiAttributes())\
179202
public func visit(_ node: \(baseKind.syntaxType)) -> \(baseKind.syntaxType) {
180-
return dispatchVisit(Syntax(node)).cast(\(baseKind.syntaxType).self)
203+
var node: Syntax = Syntax(node)
204+
dispatchVisit(&node)
205+
return node.cast(\(baseKind.syntaxType).self)
181206
}
182207
"""
183208
)
@@ -187,21 +212,16 @@ let syntaxRewriterFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
187212
"""
188213
/// Interpret `node` as a node of type `nodeType`, visit it, calling
189214
/// the `visit` to transform the node.
215+
@inline(__always)
190216
private func visitImpl<NodeType: SyntaxProtocol>(
191-
_ node: Syntax,
217+
_ node: inout Syntax,
192218
_ nodeType: NodeType.Type,
193219
_ 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))
220+
) {
221+
let origNode = node
222+
visitPre(origNode)
223+
node = visitAny(origNode) ?? Syntax(visit(origNode.cast(NodeType.self)))
224+
visitPost(origNode)
205225
}
206226
"""
207227
)
@@ -242,26 +262,26 @@ let syntaxRewriterFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
242262
/// that determines the correct visitation function will be popped of the
243263
/// stack before the function is being called, making the switch's stack
244264
/// space transient instead of having it linger in the call stack.
245-
private func visitationFunc(for node: Syntax) -> ((Syntax) -> Syntax)
265+
private func visitationFunc(for node: Syntax) -> ((inout Syntax) -> Void)
246266
"""
247267
) {
248268
try SwitchExprSyntax("switch node.raw.kind") {
249269
SwitchCaseSyntax("case .token:") {
250-
StmtSyntax("return { self.visitImpl($0, TokenSyntax.self, self.visit) }")
270+
StmtSyntax("return { self.visitImpl(&$0, TokenSyntax.self, self.visit) }")
251271
}
252272

253273
for node in NON_BASE_SYNTAX_NODES {
254274
SwitchCaseSyntax("case .\(node.varOrCaseName):") {
255-
StmtSyntax("return { self.visitImpl($0, \(node.kind.syntaxType).self, self.visit) }")
275+
StmtSyntax("return { self.visitImpl(&$0, \(node.kind.syntaxType).self, self.visit) }")
256276
}
257277
}
258278
}
259279
}
260280

261281
DeclSyntax(
262282
"""
263-
private func dispatchVisit(_ node: Syntax) -> Syntax {
264-
return visitationFunc(for: node)(node)
283+
private func dispatchVisit(_ node: inout Syntax) {
284+
visitationFunc(for: node)(&node)
265285
}
266286
"""
267287
)
@@ -272,15 +292,15 @@ let syntaxRewriterFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
272292
poundKeyword: .poundElseToken(),
273293
elements: .statements(
274294
CodeBlockItemListSyntax {
275-
try! FunctionDeclSyntax("private func dispatchVisit(_ node: Syntax) -> Syntax") {
295+
try! FunctionDeclSyntax("private func dispatchVisit(_ node: inout Syntax)") {
276296
try SwitchExprSyntax("switch node.raw.kind") {
277297
SwitchCaseSyntax("case .token:") {
278-
StmtSyntax("return visitImpl(node, TokenSyntax.self, visit)")
298+
StmtSyntax("return visitImpl(&node, TokenSyntax.self, visit)")
279299
}
280300

281301
for node in NON_BASE_SYNTAX_NODES {
282302
SwitchCaseSyntax("case .\(node.varOrCaseName):") {
283-
StmtSyntax("return visitImpl(node, \(node.kind.syntaxType).self, visit)")
303+
StmtSyntax("return visitImpl(&node, \(node.kind.syntaxType).self, visit)")
284304
}
285305
}
286306
}
@@ -307,9 +327,9 @@ let syntaxRewriterFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
307327
// nodes are being collected.
308328
var newLayout: ContiguousArray<RawSyntax?>?
309329
310-
// Rewritten children just to keep their 'SyntaxArena' alive until they are
311-
// wrapped with 'Syntax'
312-
var rewrittens: ContiguousArray<Syntax> = []
330+
// Keep 'SyntaxArena' of rewritten nodes alive until they are wrapped
331+
// with 'Syntax'
332+
var rewrittens: ContiguousArray<RetainedSyntaxArena> = []
313333
314334
let syntaxNode = node._syntaxNode
315335
@@ -329,10 +349,18 @@ let syntaxRewriterFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
329349
}
330350
331351
// Build the Syntax node to rewrite
332-
let absoluteRaw = AbsoluteRawSyntax(raw: child, info: info)
352+
var childNode: Syntax
353+
if !recyclableNodeInfos.isEmpty {
354+
let recycledInfo: Syntax.Info = recyclableNodeInfos.removeLast()!
355+
recycledInfo.info = .nonRoot(.init(parent: Syntax(node), absoluteInfo: info))
356+
childNode = Syntax(child, info: recycledInfo)
357+
} else {
358+
let absoluteRaw = AbsoluteRawSyntax(raw: child, info: info)
359+
childNode = Syntax(absoluteRaw, parent: syntaxNode)
360+
}
333361
334-
let rewritten = dispatchVisit(Syntax(absoluteRaw, parent: syntaxNode))
335-
if rewritten.id != info.nodeId {
362+
dispatchVisit(&childNode)
363+
if childNode.raw.id != child.id {
336364
// The node was rewritten, let's handle it
337365
if newLayout == nil {
338366
// We have not yet collected any previous rewritten nodes. Initialize
@@ -350,15 +378,24 @@ let syntaxRewriterFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
350378
351379
// Now that we know we have a new layout in which we collect rewritten
352380
// nodes, add it.
353-
rewrittens.append(rewritten)
354-
newLayout!.append(rewritten.raw)
381+
rewrittens.append(childNode.raw.arenaReference.retained)
382+
newLayout!.append(childNode.raw)
355383
} else {
356384
// The node was not changed by the rewriter. Only store it if a previous
357385
// node has been rewritten and we are collecting a rewritten layout.
358386
if newLayout != nil {
359387
newLayout!.append(raw)
360388
}
361389
}
390+
391+
if recyclableNodeInfos.capacity > recyclableNodeInfos.count,
392+
isKnownUniquelyReferenced(&childNode.info) {
393+
var info: Syntax.Info! = nil
394+
// 'swap' to avoid ref-counting traffic.
395+
swap(&childNode.info, &info)
396+
info.info = nil
397+
recyclableNodeInfos.append(info)
398+
}
362399
}
363400
364401
if let newLayout {

0 commit comments

Comments
 (0)