Skip to content

Commit 52b02a0

Browse files
committed
Performance improvements for SyntaxRewriter
The main improvement (among others) is that we allow the rewriter to return nil if it does not want to rewrite a node. If no child of a node is rewritten, we don’t need to replace its layout.
1 parent 0a55b1d commit 52b02a0

File tree

3 files changed

+159
-34
lines changed

3 files changed

+159
-34
lines changed

Sources/SwiftSyntax/SyntaxRewriter.swift.gyb

Lines changed: 154 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -27,18 +27,51 @@
2727

2828
open class SyntaxRewriter {
2929
public init() {}
30+
31+
/// Rewrite the given syntax tree.
32+
/// - Parameter node: The syntax tree to rewrite
33+
/// - Returns: The rewritten syntax tree
34+
public func rewrite(_ node: Syntax) -> Syntax {
35+
return visit(node) ?? node
36+
}
37+
3038
% for node in SYNTAX_NODES:
3139
% if is_visitable(node):
32-
open func visit(_ node: ${node.name}) -> ${node.base_type} {
33-
% cast = ('as! ' + node.base_type) if node.base_type != 'Syntax' else ''
40+
/// Visit a `${node.name}`. If it shall be rewritten, return the rewritten
41+
/// value. If the node shall not be rewritten, return `nil`.
42+
/// - note: Returning `nil` for a non-rewritten node is more performant than
43+
/// returning `node` itself.
44+
/// - Parameter node: the node that is being visited
45+
/// - Returns: the rewritten node or `nil` if the node shall not be replaced
46+
/// in the rewritten tree.
47+
open func visit(_ node: ${node.name}) -> ${node.base_type}? {
48+
% cast = ('as! ' + node.base_type + '?') if node.base_type != 'Syntax' else ''
3449
return visitChildren(node) ${cast}
3550
}
3651

3752
% end
3853
% end
3954

40-
open func visit(_ token: TokenSyntax) -> Syntax {
41-
return token
55+
/// Visit a `TokenSyntax`. If it shall be rewritten, return the rewritten
56+
/// value. If the node shall not be rewritten, return `nil`.
57+
/// - note: Returning `nil` for a non-rewritten node is more performant than
58+
/// returning `node` itself.
59+
/// - Parameter node: the node that is being visited
60+
/// - Returns: the rewritten node or `nil` if the node shall not be replaced
61+
/// in the rewritten tree.
62+
open func visit(_ token: TokenSyntax) -> Syntax? {
63+
return nil
64+
}
65+
66+
/// Visit a `UnknownSyntax`. If it shall be rewritten, return the rewritten
67+
/// value. If the node shall not be rewritten, return `nil`.
68+
/// - note: Returning `nil` for a non-rewritten node is more performant than
69+
/// returning `node` itself.
70+
/// - Parameter node: the node that is being visited
71+
/// - Returns: the rewritten node or `nil` if the node shall not be replaced
72+
/// in the rewritten tree.
73+
open func visit(_ node: UnknownSyntax) -> Syntax? {
74+
return visitChildren(node)
4275
}
4376

4477
/// The function called before visiting the node and its descendents.
@@ -49,7 +82,9 @@ open class SyntaxRewriter {
4982
/// specialized `visit(_:)` methods. Use this instead of those methods if
5083
/// you intend to dynamically dispatch rewriting behavior.
5184
/// - note: If this method returns a non-nil result, the specialized
52-
/// `visit(_:)` methods will not be called for this node.
85+
/// `visit(_:)` methods will not be called for this node and the
86+
/// visited node will be replaced by the returned node in the
87+
/// rewritten tree.
5388
open func visitAny(_ node: Syntax) -> Syntax? {
5489
return nil
5590
}
@@ -58,44 +93,133 @@ open class SyntaxRewriter {
5893
/// - node: the node we just finished visiting.
5994
open func visitPost(_ node: Syntax) {}
6095

61-
public func visit(_ node: Syntax) -> Syntax {
62-
visitPre(node)
63-
defer { visitPost(node) }
64-
65-
// If the global visitor returned non-nil, skip specialized dispatch.
66-
if let newNode = visitAny(node) {
67-
return newNode
68-
}
96+
/// Visit any Syntax node. If the node has been rewritten, the rewritten node
97+
/// is returned. If no rewrite occurred, `nil` is returned.
98+
/// - note: Use `rewrite` to retrieve the rewritten node or the current node
99+
/// if no rewrite occurred.
100+
/// - Parameter node: the node that is being visited
101+
/// - Returns: the rewritten node or `nil` if the node has not been
102+
/// rewritten
103+
public func visit(_ node: Syntax) -> Syntax? {
104+
return visit(node.base.data)
105+
}
69106

70-
switch node.raw.kind {
71-
case .token: return visit(node as! TokenSyntax)
72107
% for node in SYNTAX_NODES:
73-
% if is_visitable(node):
74-
case .${node.swift_syntax_kind}: return visit(node as! ${node.name})
108+
/// Implementation detail of visit(_:). Do not call directly.
109+
private func visitImpl${node.name}(_ data: SyntaxData) -> Syntax? {
110+
% if node.is_base():
111+
let node = Unknown${node.name}(data)
112+
visitPre(node)
113+
defer { visitPost(node) }
114+
if let newNode = visitAny(node) { return newNode }
115+
return visit(node)
116+
% else:
117+
let node = ${node.name}(data)
118+
visitPre(node)
119+
defer { visitPost(node) }
120+
if let newNode = visitAny(node) { return newNode }
121+
return visit(node)
75122
% end
123+
}
124+
76125
% end
77-
default: return visitChildren(node)
126+
127+
final func visit(_ data: SyntaxData) -> Syntax? {
128+
// Create the node types directly instead of going through `makeSyntax()`
129+
// which has additional cost for casting back and forth from `_SyntaxBase`.
130+
switch data.raw.kind {
131+
case .token:
132+
let node = TokenSyntax(data)
133+
visitPre(node)
134+
defer { visitPost(node) }
135+
if let newNode = visitAny(node) { return newNode }
136+
return visit(node)
137+
case .unknown:
138+
let node = UnknownSyntax(data)
139+
visitPre(node)
140+
defer { visitPost(node) }
141+
if let newNode = visitAny(node) { return newNode }
142+
return visit(node)
143+
// The implementation of every generated case goes into its own function. This
144+
// circumvents an issue where the compiler allocates stack space for every
145+
// case statement next to each other in debug builds, causing it to allocate
146+
// ~50KB per call to this function. rdar://55929175
147+
% for node in SYNTAX_NODES:
148+
case .${node.swift_syntax_kind}:
149+
return visitImpl${node.name}(data)
150+
% end
78151
}
79152
}
80153

81-
func visitChildren(_ nodeS: Syntax) -> Syntax {
82-
// Visit all children of this node, returning `nil` if child is not
83-
// present. This will ensure that there are always the same number
84-
// of children after transforming.
154+
final func visitChildren(_ nodeS: Syntax) -> Syntax? {
85155
let node = nodeS.base
86-
let newLayout = RawSyntaxChildren(node).map { (n: (RawSyntax?, AbsoluteSyntaxInfo)) -> RawSyntax? in
87-
let (raw, info) = n
88-
guard let child = raw else { return nil }
156+
157+
// Walk over all children of this node and rewrite them. Don't store any
158+
// rewritten nodes until the first non-`nil` value is encountered. When this
159+
// happens, retrieve all previous syntax nodes from the parent node to
160+
// initialize the new layout. Once we know that we have to rewrite the
161+
// layout, we need to collect all futher children, regardless of whether
162+
// they are rewritten or not.
163+
164+
// newLayout is nil until the first child node is rewritten and rewritten
165+
// nodes are being collected.
166+
var newLayout: ContiguousArray<RawSyntax?>?
167+
168+
for (i, (raw, info)) in RawSyntaxChildren(node).enumerated() {
169+
guard let child = raw else {
170+
// Node does not exist. If we are collecting rewritten nodes, we need to
171+
// collect this one as well, otherwise we can ignore it.
172+
if newLayout != nil {
173+
newLayout!.append(nil)
174+
}
175+
continue
176+
}
177+
178+
// Build the Syntax node to rewrite
89179
let absoluteRaw = AbsoluteRawSyntax(raw: child, info: info)
90180
let data = SyntaxData(absoluteRaw, parent: node)
91-
return visit(makeSyntax(data)).raw
181+
182+
if let rewritten = visit(data)?.raw {
183+
// The node was rewritten, let's handle it
184+
if newLayout == nil {
185+
// We have not yet collected any previous rewritten nodes. Initialize
186+
// the new layout with the previous nodes of the parent. This is
187+
// possible, since we know they were not rewritten.
188+
189+
// The below implementation is based on Collection.map but directly
190+
// reserves enough capacity for the entire layout.
191+
newLayout = ContiguousArray<RawSyntax?>()
192+
newLayout!.reserveCapacity(node.raw.numberOfChildren)
193+
for j in 0..<i {
194+
newLayout!.append(node.raw.child(at: j))
195+
}
196+
}
197+
198+
// Now that we know we have a new layout in which we collect rewritten
199+
// nodes, add it.
200+
newLayout!.append(rewritten)
201+
} else {
202+
// The node was not changed by the rewriter. Only store it if a previous
203+
// node has been rewritten and we are collecting a rewritten layout.
204+
if newLayout != nil {
205+
newLayout!.append(raw)
206+
}
207+
}
92208
}
93209

94-
// Sanity check, ensure the new children are the same length.
95-
assert(newLayout.count == node.raw.numberOfChildren)
210+
if let newLayout = newLayout {
211+
// A child node was rewritten. Build the updated node.
212+
213+
// Sanity check, ensure the new children are the same length.
214+
assert(newLayout.count == node.raw.numberOfChildren)
215+
216+
let newRaw = node.raw.replacingLayout(Array(newLayout))
217+
return makeSyntax(.forRoot(newRaw))
218+
} else {
219+
// No child node was rewritten. So no need to change this node as well.
220+
return nil
221+
}
96222

97-
let newRaw = node.raw.replacingLayout(newLayout)
98-
return makeSyntax(.forRoot(newRaw))
99223
}
100224
}
101225

Tests/SwiftSyntaxTest/AbsolutePosition.swift

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ import XCTest
22
import SwiftSyntax
33

44
fileprivate class FuncRenamer: SyntaxRewriter {
5-
override func visit(_ node: FunctionDeclSyntax) ->DeclSyntax {
6-
return (super.visit(node) as! FunctionDeclSyntax).withIdentifier(
5+
override func visit(_ node: FunctionDeclSyntax) -> DeclSyntax {
6+
let rewritten = super.visit(node) ?? node
7+
return (rewritten as! FunctionDeclSyntax).withIdentifier(
78
SyntaxFactory.makeIdentifier("anotherName"))
89
}
910
}

Tests/SwiftSyntaxTest/VisitorTest.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ public class SyntaxVisitorTestCase: XCTestCase {
3939
XCTAssertNoThrow(try {
4040
let parsed = try SyntaxParser.parse(getInput("closure.swift"))
4141
let rewriter = ClosureRewriter()
42-
let rewritten = rewriter.visit(parsed)
42+
let rewritten = rewriter.rewrite(parsed)
4343
XCTAssertEqual(parsed.description, rewritten.description)
4444
}())
4545
}
@@ -62,7 +62,7 @@ public class SyntaxVisitorTestCase: XCTestCase {
6262
let rewriter = VisitAnyRewriter(transform: { _ in
6363
return SyntaxFactory.makeIdentifier("")
6464
})
65-
let rewritten = rewriter.visit(parsed)
65+
let rewritten = rewriter.rewrite(parsed)
6666
XCTAssertEqual(rewritten.description, "")
6767
}())
6868
}

0 commit comments

Comments
 (0)