@@ -39,6 +39,20 @@ let syntaxRewriterFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
39
39
/// tree, all of the rewritten node's parents also need to be re-created. This is the arena in which those
40
40
/// intermediate nodes should be allocated.
41
41
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?>
42
56
"""
43
57
)
44
58
@@ -47,6 +61,8 @@ let syntaxRewriterFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
47
61
public init(viewMode: SyntaxTreeViewMode = .sourceAccurate) {
48
62
self.viewMode = viewMode
49
63
self.arena = nil
64
+ self.recyclableNodeInfos = []
65
+ self.recyclableNodeInfos.reserveCapacity(64)
50
66
}
51
67
"""
52
68
)
@@ -57,6 +73,8 @@ let syntaxRewriterFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
57
73
public init(viewMode: SyntaxTreeViewMode = .sourceAccurate, arena: SyntaxArena? = nil) {
58
74
self.viewMode = viewMode
59
75
self.arena = arena
76
+ self.recyclableNodeInfos = []
77
+ self.recyclableNodeInfos.reserveCapacity(64)
60
78
}
61
79
"""
62
80
)
@@ -65,7 +83,8 @@ let syntaxRewriterFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
65
83
"""
66
84
/// Rewrite `node`, keeping its parent unless `detach` is `true`.
67
85
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)
69
88
if detach {
70
89
return rewritten
71
90
}
@@ -126,15 +145,19 @@ let syntaxRewriterFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
126
145
/// - Returns: the rewritten node
127
146
@available(*, deprecated, renamed: " rewrite(_:detach:) " )
128
147
public func visit(_ node: Syntax) -> Syntax {
129
- return dispatchVisit(node)
148
+ var rewritten = node
149
+ dispatchVisit(&rewritten)
150
+ return rewritten
130
151
}
131
152
"""
132
153
)
133
154
134
155
DeclSyntax (
135
156
"""
136
157
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)
138
161
}
139
162
"""
140
163
)
@@ -177,7 +200,9 @@ let syntaxRewriterFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
177
200
/// - Returns: the rewritten node
178
201
\( baseNode. apiAttributes ( ) ) \
179
202
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)
181
206
}
182
207
"""
183
208
)
@@ -187,21 +212,16 @@ let syntaxRewriterFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
187
212
"""
188
213
/// Interpret `node` as a node of type `nodeType`, visit it, calling
189
214
/// the `visit` to transform the node.
215
+ @inline(__always)
190
216
private func visitImpl<NodeType: SyntaxProtocol>(
191
- _ node: Syntax,
217
+ _ node: inout Syntax,
192
218
_ nodeType: NodeType.Type,
193
219
_ 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)
205
225
}
206
226
"""
207
227
)
@@ -242,26 +262,26 @@ let syntaxRewriterFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
242
262
/// that determines the correct visitation function will be popped of the
243
263
/// stack before the function is being called, making the switch's stack
244
264
/// 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 )
246
266
"""
247
267
) {
248
268
try SwitchExprSyntax ( " switch node.raw.kind " ) {
249
269
SwitchCaseSyntax ( " case .token: " ) {
250
- StmtSyntax ( " return { self.visitImpl($0, TokenSyntax.self, self.visit) } " )
270
+ StmtSyntax ( " return { self.visitImpl(& $0, TokenSyntax.self, self.visit) } " )
251
271
}
252
272
253
273
for node in NON_BASE_SYNTAX_NODES {
254
274
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) } " )
256
276
}
257
277
}
258
278
}
259
279
}
260
280
261
281
DeclSyntax (
262
282
"""
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)
265
285
}
266
286
"""
267
287
)
@@ -272,15 +292,15 @@ let syntaxRewriterFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
272
292
poundKeyword: . poundElseToken( ) ,
273
293
elements: . statements(
274
294
CodeBlockItemListSyntax {
275
- try ! FunctionDeclSyntax ( " private func dispatchVisit(_ node: Syntax) -> Syntax " ) {
295
+ try ! FunctionDeclSyntax ( " private func dispatchVisit(_ node: inout Syntax) " ) {
276
296
try SwitchExprSyntax ( " switch node.raw.kind " ) {
277
297
SwitchCaseSyntax ( " case .token: " ) {
278
- StmtSyntax ( " return visitImpl(node, TokenSyntax.self, visit) " )
298
+ StmtSyntax ( " return visitImpl(& node, TokenSyntax.self, visit) " )
279
299
}
280
300
281
301
for node in NON_BASE_SYNTAX_NODES {
282
302
SwitchCaseSyntax ( " case . \( node. varOrCaseName) : " ) {
283
- StmtSyntax ( " return visitImpl(node, \( node. kind. syntaxType) .self, visit) " )
303
+ StmtSyntax ( " return visitImpl(& node, \( node. kind. syntaxType) .self, visit) " )
284
304
}
285
305
}
286
306
}
@@ -307,9 +327,9 @@ let syntaxRewriterFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
307
327
// nodes are being collected.
308
328
var newLayout: ContiguousArray<RawSyntax?>?
309
329
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 > = []
313
333
314
334
let syntaxNode = node._syntaxNode
315
335
@@ -329,10 +349,18 @@ let syntaxRewriterFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
329
349
}
330
350
331
351
// 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
+ }
333
361
334
- let rewritten = dispatchVisit(Syntax(absoluteRaw, parent: syntaxNode) )
335
- if rewritten. id != info.nodeId {
362
+ dispatchVisit(&childNode )
363
+ if childNode.raw. id != child.id {
336
364
// The node was rewritten, let's handle it
337
365
if newLayout == nil {
338
366
// We have not yet collected any previous rewritten nodes. Initialize
@@ -350,15 +378,24 @@ let syntaxRewriterFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
350
378
351
379
// Now that we know we have a new layout in which we collect rewritten
352
380
// nodes, add it.
353
- rewrittens.append(rewritten )
354
- newLayout!.append(rewritten .raw)
381
+ rewrittens.append(childNode.raw.arenaReference.retained )
382
+ newLayout!.append(childNode .raw)
355
383
} else {
356
384
// The node was not changed by the rewriter. Only store it if a previous
357
385
// node has been rewritten and we are collecting a rewritten layout.
358
386
if newLayout != nil {
359
387
newLayout!.append(raw)
360
388
}
361
389
}
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
+ }
362
399
}
363
400
364
401
if let newLayout {
0 commit comments