@@ -32,6 +32,24 @@ let syntaxVisitorFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
3232 try ! ClassDeclSyntax ( " open class SyntaxVisitor " ) {
3333 DeclSyntax ( " public let viewMode: SyntaxTreeViewMode " )
3434
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+
3553 DeclSyntax (
3654 """
3755 public init(viewMode: SyntaxTreeViewMode) {
@@ -45,7 +63,8 @@ let syntaxVisitorFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
4563 /// Walk all nodes of the given syntax tree, calling the corresponding `visit`
4664 /// function for every node that is being visited.
4765 public func walk(_ node: some SyntaxProtocol) {
48- visit(Syntax(node))
66+ var syntaxNode = Syntax(node)
67+ visit(&syntaxNode)
4968 }
5069 """
5170 )
@@ -94,21 +113,30 @@ let syntaxVisitorFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
94113
95114 DeclSyntax (
96115 """
97- /// Interpret `data` as a node of type `nodeType`, visit it, calling
116+ /// Cast `node` to a node of type `nodeType`, visit it, calling
98117 /// 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)
99125 private func visitImpl<NodeType: SyntaxProtocol>(
100- _ node: Syntax,
126+ _ node: inout Syntax,
101127 _ nodeType: NodeType.Type,
102128 _ visit: (NodeType) -> SyntaxVisitorContinueKind,
103129 _ visitPost: (NodeType) -> Void
104130 ) {
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)
107135 // Avoid calling into visitChildren if possible.
108136 if needsChildren && !node.raw.layoutView!.children.isEmpty {
109- visitChildren(node)
137+ visitChildren(& node)
110138 }
111- visitPost(node )
139+ visitPost(castedNode )
112140 }
113141 """
114142 )
@@ -149,7 +177,7 @@ let syntaxVisitorFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
149177 /// that determines the correct visitation function will be popped of the
150178 /// stack before the function is being called, making the switch's stack
151179 /// 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)
153181 """
154182 ) {
155183 try SwitchExprSyntax ( " switch node.raw.kind " ) {
@@ -168,16 +196,16 @@ let syntaxVisitorFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
168196
169197 for node in NON_BASE_SYNTAX_NODES {
170198 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) } " )
172200 }
173201 }
174202 }
175203 }
176204
177205 DeclSyntax (
178206 """
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)
181209 }
182210 """
183211 )
@@ -188,7 +216,12 @@ let syntaxVisitorFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
188216 poundKeyword: . poundElseToken( ) ,
189217 elements: . statements(
190218 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+ ) {
192225 try SwitchExprSyntax ( " switch node.raw.kind " ) {
193226 SwitchCaseSyntax ( " case .token: " ) {
194227 DeclSyntax ( " let node = node.cast(TokenSyntax.self) " )
@@ -203,7 +236,7 @@ let syntaxVisitorFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
203236
204237 for node in NON_BASE_SYNTAX_NODES {
205238 SwitchCaseSyntax ( " case . \( node. varOrCaseName) : " ) {
206- ExprSyntax ( " visitImpl(node, \( node. kind. syntaxType) .self, visit, visitPost) " )
239+ ExprSyntax ( " visitImpl(& node, \( node. kind. syntaxType) .self, visit, visitPost) " )
207240 }
208241 }
209242 }
@@ -217,10 +250,31 @@ let syntaxVisitorFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
217250
218251 DeclSyntax (
219252 """
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) {
222255 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+ }
224278 }
225279 }
226280 """
0 commit comments