@@ -32,6 +32,24 @@ 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. It shouldn't be much larger because that would mean that we need to
45
+ /// look through more memory to find a cache miss. 40 has been chosen empirically to strike a good balance here.
46
+ ///
47
+ /// The actual `info` stored in the `Syntax.Info` objects is garbage. It needs to be set when any of the `Syntax.Info`
48
+ /// objects get re-used.
49
+ private var recyclableNodeInfos: ContiguousArray<Syntax.Info?> = ContiguousArray(repeating: nil, count: 40)
50
+ """
51
+ )
52
+
35
53
DeclSyntax (
36
54
"""
37
55
public init(viewMode: SyntaxTreeViewMode) {
@@ -45,7 +63,8 @@ let syntaxVisitorFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
45
63
/// Walk all nodes of the given syntax tree, calling the corresponding `visit`
46
64
/// function for every node that is being visited.
47
65
public func walk(_ node: some SyntaxProtocol) {
48
- visit(Syntax(node))
66
+ var syntaxNode = Syntax(node)
67
+ visit(&syntaxNode)
49
68
}
50
69
"""
51
70
)
@@ -94,21 +113,30 @@ let syntaxVisitorFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
94
113
95
114
DeclSyntax (
96
115
"""
97
- /// Interpret `data` as a node of type `nodeType`, visit it, calling
116
+ /// Cast `node` to a node of type `nodeType`, visit it, calling
98
117
/// the `visit` and `visitPost` functions during visitation.
118
+ ///
119
+ /// - Note: node is an `inout` parameter so that callers don't have to retain it before passing it to `visitImpl`.
120
+ /// With it being an `inout` parameter, the caller and `visitImpl` can work on the same reference of `node` without
121
+ /// any reference counting.
122
+ /// - Note: Inline so that the optimizer can look through the calles to `visit` and `visitPost`, which means it
123
+ /// doesn't need to retain `self` when forming closures to the unapplied function references on `self`.
124
+ @inline(__always)
99
125
private func visitImpl<NodeType: SyntaxProtocol>(
100
- _ node: Syntax,
126
+ _ node: inout Syntax,
101
127
_ nodeType: NodeType.Type,
102
128
_ visit: (NodeType) -> SyntaxVisitorContinueKind,
103
129
_ visitPost: (NodeType) -> Void
104
130
) {
105
- let node = node.cast(NodeType.self)
106
- let needsChildren = (visit(node) == .visitChildren)
131
+ let castedNode = node.cast(NodeType.self)
132
+ // We retain castedNode.info here before passing it to visit.
133
+ // I don't think that's necessary because castedNode is already retained but don't know how to prevent it.
134
+ let needsChildren = (visit(castedNode) == .visitChildren)
107
135
// Avoid calling into visitChildren if possible.
108
136
if needsChildren && !node.raw.layoutView!.children.isEmpty {
109
- visitChildren(node)
137
+ visitChildren(& node)
110
138
}
111
- visitPost(node )
139
+ visitPost(castedNode )
112
140
}
113
141
"""
114
142
)
@@ -149,7 +177,7 @@ let syntaxVisitorFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
149
177
/// that determines the correct visitation function will be popped of the
150
178
/// stack before the function is being called, making the switch's stack
151
179
/// space transient instead of having it linger in the call stack.
152
- private func visitationFunc(for node: Syntax) -> ((Syntax) -> Void)
180
+ private func visitationFunc(for node: Syntax) -> ((inout Syntax) -> Void)
153
181
"""
154
182
) {
155
183
try SwitchExprSyntax ( " switch node.raw.kind " ) {
@@ -168,16 +196,16 @@ let syntaxVisitorFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
168
196
169
197
for node in NON_BASE_SYNTAX_NODES {
170
198
SwitchCaseSyntax ( " case . \( node. varOrCaseName) : " ) {
171
- StmtSyntax ( " return { self.visitImpl($0, \( node. kind. syntaxType) .self, self.visit, self.visitPost) } " )
199
+ StmtSyntax ( " return { self.visitImpl(& $0, \( node. kind. syntaxType) .self, self.visit, self.visitPost) } " )
172
200
}
173
201
}
174
202
}
175
203
}
176
204
177
205
DeclSyntax (
178
206
"""
179
- private func visit(_ node: Syntax) {
180
- return visitationFunc(for: node)(node)
207
+ private func visit(_ node: inout Syntax) {
208
+ return visitationFunc(for: node)(& node)
181
209
}
182
210
"""
183
211
)
@@ -188,7 +216,12 @@ let syntaxVisitorFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
188
216
poundKeyword: . poundElseToken( ) ,
189
217
elements: . statements(
190
218
CodeBlockItemListSyntax {
191
- try ! FunctionDeclSyntax ( " private func visit(_ node: Syntax) " ) {
219
+ try ! FunctionDeclSyntax (
220
+ """
221
+ /// - Note: `node` is `inout` to avoid ref-counting. See comment in `visitImpl`
222
+ private func visit(_ node: inout Syntax)
223
+ """
224
+ ) {
192
225
try SwitchExprSyntax ( " switch node.raw.kind " ) {
193
226
SwitchCaseSyntax ( " case .token: " ) {
194
227
DeclSyntax ( " let node = node.cast(TokenSyntax.self) " )
@@ -203,7 +236,7 @@ let syntaxVisitorFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
203
236
204
237
for node in NON_BASE_SYNTAX_NODES {
205
238
SwitchCaseSyntax ( " case . \( node. varOrCaseName) : " ) {
206
- ExprSyntax ( " visitImpl(node, \( node. kind. syntaxType) .self, visit, visitPost) " )
239
+ ExprSyntax ( " visitImpl(& node, \( node. kind. syntaxType) .self, visit, visitPost) " )
207
240
}
208
241
}
209
242
}
@@ -217,10 +250,31 @@ let syntaxVisitorFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
217
250
218
251
DeclSyntax (
219
252
"""
220
- private func visitChildren(_ node: some SyntaxProtocol) {
221
- let syntaxNode = Syntax(node)
253
+ /// - Note: ` node` is `inout` to avoid reference counting. See comment in `visitImpl`.
254
+ private func visitChildren(_ syntaxNode: inout Syntax) {
222
255
for childRaw in NonNilRawSyntaxChildren(syntaxNode, viewMode: viewMode) {
223
- visit(Syntax(childRaw, parent: syntaxNode))
256
+ // syntaxNode gets retained here. That seems unnecessary but I don't know how to remove it.
257
+ var childNode: Syntax
258
+ if let recycledInfoIndex = recyclableNodeInfos.firstIndex(where: { $0 != nil }) {
259
+ var recycledInfo: Syntax.Info? = nil
260
+ // Use `swap` to extract the recyclable syntax node without incurring ref-counting.
261
+ swap(&recycledInfo, &recyclableNodeInfos[recycledInfoIndex])
262
+ // syntaxNode.info gets retained here. This is necessary because we build up the parent tree.
263
+ recycledInfo!.info = .nonRoot(.init(parent: syntaxNode, absoluteInfo: childRaw.info))
264
+ childNode = Syntax(childRaw.raw, info: recycledInfo!)
265
+ } else {
266
+ childNode = Syntax(childRaw, parent: syntaxNode)
267
+ }
268
+ visit(&childNode)
269
+ if isKnownUniquelyReferenced(&childNode.info) {
270
+ // The node didn't get stored by the subclass's visit method. We can re-use the memory of its `Syntax.Info`
271
+ // for future syntax nodes.
272
+ childNode.info.info = nil
273
+ if let emptySlot = recyclableNodeInfos.firstIndex(where: { $0 == nil }) {
274
+ // Use `swap` to store the recyclable syntax node without incurring ref-counting.
275
+ swap(&recyclableNodeInfos[emptySlot], &childNode.info)
276
+ }
277
+ }
224
278
}
225
279
}
226
280
"""
0 commit comments