@@ -32,6 +32,32 @@ let syntaxVisitorFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
32
32
try ! ClassDeclSyntax ( " open class SyntaxVisitor " ) {
33
33
DeclSyntax ( " public let viewMode: SyntaxTreeViewMode " )
34
34
35
+ DeclSyntax (
36
+ """
37
+ /// `Syntax.Info` objects created in `visitChildren` but whose `Syntax` nodes were not retained by the `visit`
38
+ /// functions implemented by a subclass of `SyntaxVisitor`.
39
+ ///
40
+ /// Instead of deallocating them and allocating memory for new syntax nodes, store the allocated memory in an array.
41
+ /// We can then re-use them to create new syntax nodes.
42
+ ///
43
+ /// The array's size should be a typical nesting depth of a Swift file. That way we can store all allocated syntax
44
+ /// nodes when unwinding the visitation stack.
45
+ ///
46
+ /// The actual `info` stored in the `Syntax.Info` objects is garbage. It needs to be set when any of the `Syntax.Info`
47
+ /// objects get re-used.
48
+ private var recyclableNodeInfos: ContiguousArray<Syntax.Info?> = ContiguousArray(repeating: nil, count: 64)
49
+ """
50
+ )
51
+
52
+ DeclSyntax (
53
+ """
54
+ /// A bit is set to 1 if the corresponding index in `recyclableNodeInfos` is occupied and ready to be reused.
55
+ ///
56
+ /// The last bit in this UInt64 corresponds to index 0 in `recyclableNodeInfos`.
57
+ private var recyclableNodeInfosUsageBitmap: UInt64 = 0
58
+ """
59
+ )
60
+
35
61
DeclSyntax (
36
62
"""
37
63
public init(viewMode: SyntaxTreeViewMode) {
@@ -45,7 +71,8 @@ let syntaxVisitorFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
45
71
/// Walk all nodes of the given syntax tree, calling the corresponding `visit`
46
72
/// function for every node that is being visited.
47
73
public func walk(_ node: some SyntaxProtocol) {
48
- visit(Syntax(node))
74
+ var syntaxNode = Syntax(node)
75
+ visit(&syntaxNode)
49
76
}
50
77
"""
51
78
)
@@ -94,21 +121,30 @@ let syntaxVisitorFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
94
121
95
122
DeclSyntax (
96
123
"""
97
- /// Interpret `data` as a node of type `nodeType`, visit it, calling
124
+ /// Cast `node` to a node of type `nodeType`, visit it, calling
98
125
/// the `visit` and `visitPost` functions during visitation.
126
+ ///
127
+ /// - Note: node is an `inout` parameter so that callers don't have to retain it before passing it to `visitImpl`.
128
+ /// With it being an `inout` parameter, the caller and `visitImpl` can work on the same reference of `node` without
129
+ /// any reference counting.
130
+ /// - Note: Inline so that the optimizer can look through the calles to `visit` and `visitPost`, which means it
131
+ /// doesn't need to retain `self` when forming closures to the unapplied function references on `self`.
132
+ @inline(__always)
99
133
private func visitImpl<NodeType: SyntaxProtocol>(
100
- _ node: Syntax,
134
+ _ node: inout Syntax,
101
135
_ nodeType: NodeType.Type,
102
136
_ visit: (NodeType) -> SyntaxVisitorContinueKind,
103
137
_ visitPost: (NodeType) -> Void
104
138
) {
105
- let node = node.cast(NodeType.self)
106
- let needsChildren = (visit(node) == .visitChildren)
139
+ let castedNode = node.cast(NodeType.self)
140
+ // We retain castedNode.info here before passing it to visit.
141
+ // I don't think that's necessary because castedNode is already retained but don't know how to prevent it.
142
+ let needsChildren = (visit(castedNode) == .visitChildren)
107
143
// Avoid calling into visitChildren if possible.
108
144
if needsChildren && !node.raw.layoutView!.children.isEmpty {
109
- visitChildren(node)
145
+ visitChildren(& node)
110
146
}
111
- visitPost(node )
147
+ visitPost(castedNode )
112
148
}
113
149
"""
114
150
)
@@ -149,7 +185,7 @@ let syntaxVisitorFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
149
185
/// that determines the correct visitation function will be popped of the
150
186
/// stack before the function is being called, making the switch's stack
151
187
/// space transient instead of having it linger in the call stack.
152
- private func visitationFunc(for node: Syntax) -> ((Syntax) -> Void)
188
+ private func visitationFunc(for node: Syntax) -> ((inout Syntax) -> Void)
153
189
"""
154
190
) {
155
191
try SwitchExprSyntax ( " switch node.raw.kind " ) {
@@ -168,16 +204,16 @@ let syntaxVisitorFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
168
204
169
205
for node in NON_BASE_SYNTAX_NODES {
170
206
SwitchCaseSyntax ( " case . \( node. varOrCaseName) : " ) {
171
- StmtSyntax ( " return { self.visitImpl($0, \( node. kind. syntaxType) .self, self.visit, self.visitPost) } " )
207
+ StmtSyntax ( " return { self.visitImpl(& $0, \( node. kind. syntaxType) .self, self.visit, self.visitPost) } " )
172
208
}
173
209
}
174
210
}
175
211
}
176
212
177
213
DeclSyntax (
178
214
"""
179
- private func visit(_ node: Syntax) {
180
- return visitationFunc(for: node)(node)
215
+ private func visit(_ node: inout Syntax) {
216
+ return visitationFunc(for: node)(& node)
181
217
}
182
218
"""
183
219
)
@@ -188,7 +224,12 @@ let syntaxVisitorFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
188
224
poundKeyword: . poundElseToken( ) ,
189
225
elements: . statements(
190
226
CodeBlockItemListSyntax {
191
- try ! FunctionDeclSyntax ( " private func visit(_ node: Syntax) " ) {
227
+ try ! FunctionDeclSyntax (
228
+ """
229
+ /// - Note: `node` is `inout` to avoid ref-counting. See comment in `visitImpl`
230
+ private func visit(_ node: inout Syntax)
231
+ """
232
+ ) {
192
233
try SwitchExprSyntax ( " switch node.raw.kind " ) {
193
234
SwitchCaseSyntax ( " case .token: " ) {
194
235
DeclSyntax ( " let node = node.cast(TokenSyntax.self) " )
@@ -203,7 +244,7 @@ let syntaxVisitorFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
203
244
204
245
for node in NON_BASE_SYNTAX_NODES {
205
246
SwitchCaseSyntax ( " case . \( node. varOrCaseName) : " ) {
206
- ExprSyntax ( " visitImpl(node, \( node. kind. syntaxType) .self, visit, visitPost) " )
247
+ ExprSyntax ( " visitImpl(& node, \( node. kind. syntaxType) .self, visit, visitPost) " )
207
248
}
208
249
}
209
250
}
@@ -217,13 +258,66 @@ let syntaxVisitorFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
217
258
218
259
DeclSyntax (
219
260
"""
220
- private func visitChildren(_ node: some SyntaxProtocol) {
221
- let syntaxNode = Syntax(node)
261
+ /// - Note: ` node` is `inout` to avoid reference counting. See comment in `visitImpl`.
262
+ private func visitChildren(_ syntaxNode: inout Syntax) {
222
263
for childRaw in NonNilRawSyntaxChildren(syntaxNode, viewMode: viewMode) {
223
- visit(Syntax(childRaw, parent: syntaxNode))
264
+ // syntaxNode gets retained here. That seems unnecessary but I don't know how to remove it.
265
+ var childNode: Syntax
266
+ if let recycledInfoIndex = recyclableNodeInfosUsageBitmap.indexOfRightmostOne {
267
+ var recycledInfo: Syntax.Info? = nil
268
+ // Use `swap` to extract the recyclable syntax node without incurring ref-counting.
269
+ swap(&recycledInfo, &recyclableNodeInfos[recycledInfoIndex])
270
+ assert(recycledInfo != nil, " Slot indicated by the bitmap did not contain a value " )
271
+ recyclableNodeInfosUsageBitmap.setBitToZero(at: recycledInfoIndex)
272
+ // syntaxNode.info gets retained here. This is necessary because we build up the parent tree.
273
+ recycledInfo!.info = .nonRoot(.init(parent: syntaxNode, absoluteInfo: childRaw.info))
274
+ childNode = Syntax(childRaw.raw, info: recycledInfo!)
275
+ } else {
276
+ childNode = Syntax(childRaw, parent: syntaxNode)
277
+ }
278
+ visit(&childNode)
279
+ if isKnownUniquelyReferenced(&childNode.info) {
280
+ // The node didn't get stored by the subclass's visit method. We can re-use the memory of its `Syntax.Info`
281
+ // for future syntax nodes.
282
+ childNode.info.info = nil
283
+ if let emptySlot = recyclableNodeInfosUsageBitmap.indexOfRightmostZero {
284
+ // Use `swap` to store the recyclable syntax node without incurring ref-counting.
285
+ swap(&recyclableNodeInfos[emptySlot], &childNode.info)
286
+ assert(childNode.info == nil, " Slot should not have contained a value " )
287
+ recyclableNodeInfosUsageBitmap.setBitToOne(at: emptySlot)
288
+ }
289
+ }
224
290
}
225
291
}
226
292
"""
227
293
)
228
294
}
295
+
296
+ DeclSyntax (
297
+ """
298
+ fileprivate extension UInt64 {
299
+ var indexOfRightmostZero: Int? {
300
+ return (~self).indexOfRightmostOne
301
+ }
302
+
303
+ var indexOfRightmostOne: Int? {
304
+ let trailingZeroCount = self.trailingZeroBitCount
305
+ if trailingZeroCount == Self.bitWidth {
306
+ // All indicies are 0
307
+ return nil
308
+ }
309
+ return trailingZeroCount
310
+ }
311
+
312
+ mutating func setBitToZero(at index: Int) {
313
+ self &= ~(1 << index)
314
+ }
315
+
316
+ mutating func setBitToOne(at index: Int) {
317
+ self |= 1 << index
318
+ }
319
+ }
320
+
321
+ """
322
+ )
229
323
}
0 commit comments