Skip to content

Commit 1d5126f

Browse files
committed
Refactor SyntaxRewriter
Make the implementation of SyntaxRewriter more similar to that of SyntaxVisitor, improve performance and make SyntaxRewriter ready for future performance improvements.
1 parent 0a55b1d commit 1d5126f

File tree

2 files changed

+128
-26
lines changed

2 files changed

+128
-26
lines changed

Sources/SwiftSyntax/SyntaxRewriter.swift.gyb

Lines changed: 127 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,12 @@
2727

2828
open class SyntaxRewriter {
2929
public init() {}
30+
3031
% for node in SYNTAX_NODES:
3132
% if is_visitable(node):
33+
/// Visit a `${node.name}`.
34+
/// - Parameter node: the node that is being visited
35+
/// - Returns: the rewritten node
3236
open func visit(_ node: ${node.name}) -> ${node.base_type} {
3337
% cast = ('as! ' + node.base_type) if node.base_type != 'Syntax' else ''
3438
return visitChildren(node) ${cast}
@@ -37,9 +41,19 @@ open class SyntaxRewriter {
3741
% end
3842
% end
3943

44+
/// Visit a `TokenSyntax`.
45+
/// - Parameter node: the node that is being visited
46+
/// - Returns: the rewritten node
4047
open func visit(_ token: TokenSyntax) -> Syntax {
4148
return token
4249
}
50+
51+
/// Visit a `UnknownSyntax`.
52+
/// - Parameter node: the node that is being visited
53+
/// - Returns: the rewritten node or `nil`.
54+
open func visit(_ node: UnknownSyntax) -> Syntax {
55+
return visitChildren(node)
56+
}
4357

4458
/// The function called before visiting the node and its descendents.
4559
/// - node: the node we are about to visit.
@@ -49,7 +63,9 @@ open class SyntaxRewriter {
4963
/// specialized `visit(_:)` methods. Use this instead of those methods if
5064
/// you intend to dynamically dispatch rewriting behavior.
5165
/// - note: If this method returns a non-nil result, the specialized
52-
/// `visit(_:)` methods will not be called for this node.
66+
/// `visit(_:)` methods will not be called for this node and the
67+
/// visited node will be replaced by the returned node in the
68+
/// rewritten tree.
5369
open func visitAny(_ node: Syntax) -> Syntax? {
5470
return nil
5571
}
@@ -58,44 +74,130 @@ open class SyntaxRewriter {
5874
/// - node: the node we just finished visiting.
5975
open func visitPost(_ node: Syntax) {}
6076

77+
/// Visit any Syntax node.
78+
/// - Parameter node: the node that is being visited
79+
/// - Returns: the rewritten node
6180
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-
}
81+
return visit(node.base.data)
82+
}
6983

70-
switch node.raw.kind {
71-
case .token: return visit(node as! TokenSyntax)
7284
% for node in SYNTAX_NODES:
73-
% if is_visitable(node):
74-
case .${node.swift_syntax_kind}: return visit(node as! ${node.name})
85+
/// Implementation detail of visit(_:). Do not call directly.
86+
private func visitImpl${node.name}(_ data: SyntaxData) -> Syntax {
87+
% if node.is_base():
88+
let node = Unknown${node.name}(data)
89+
visitPre(node)
90+
defer { visitPost(node) }
91+
if let newNode = visitAny(node) { return newNode }
92+
return visit(node)
93+
% else:
94+
let node = ${node.name}(data)
95+
visitPre(node)
96+
defer { visitPost(node) }
97+
if let newNode = visitAny(node) { return newNode }
98+
return visit(node)
7599
% end
100+
}
101+
76102
% end
77-
default: return visitChildren(node)
103+
104+
final func visit(_ data: SyntaxData) -> Syntax {
105+
// Create the node types directly instead of going through `makeSyntax()`
106+
// which has additional cost for casting back and forth from `_SyntaxBase`.
107+
switch data.raw.kind {
108+
case .token:
109+
let node = TokenSyntax(data)
110+
visitPre(node)
111+
defer { visitPost(node) }
112+
if let newNode = visitAny(node) { return newNode }
113+
return visit(node)
114+
case .unknown:
115+
let node = UnknownSyntax(data)
116+
visitPre(node)
117+
defer { visitPost(node) }
118+
if let newNode = visitAny(node) { return newNode }
119+
return visit(node)
120+
// The implementation of every generated case goes into its own function. This
121+
// circumvents an issue where the compiler allocates stack space for every
122+
// case statement next to each other in debug builds, causing it to allocate
123+
// ~50KB per call to this function. rdar://55929175
124+
% for node in SYNTAX_NODES:
125+
case .${node.swift_syntax_kind}:
126+
return visitImpl${node.name}(data)
127+
% end
78128
}
79129
}
80130

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.
131+
final func visitChildren(_ nodeS: Syntax) -> Syntax {
85132
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 }
133+
134+
// Walk over all children of this node and rewrite them. Don't store any
135+
// rewritten nodes until the first non-`nil` value is encountered. When this
136+
// happens, retrieve all previous syntax nodes from the parent node to
137+
// initialize the new layout. Once we know that we have to rewrite the
138+
// layout, we need to collect all futher children, regardless of whether
139+
// they are rewritten or not.
140+
141+
// newLayout is nil until the first child node is rewritten and rewritten
142+
// nodes are being collected.
143+
var newLayout: ContiguousArray<RawSyntax?>?
144+
145+
for (i, (raw, info)) in RawSyntaxChildren(node).enumerated() {
146+
guard let child = raw else {
147+
// Node does not exist. If we are collecting rewritten nodes, we need to
148+
// collect this one as well, otherwise we can ignore it.
149+
if newLayout != nil {
150+
newLayout!.append(nil)
151+
}
152+
continue
153+
}
154+
155+
// Build the Syntax node to rewrite
89156
let absoluteRaw = AbsoluteRawSyntax(raw: child, info: info)
90157
let data = SyntaxData(absoluteRaw, parent: node)
91-
return visit(makeSyntax(data)).raw
158+
159+
let rewritten = visit(data)
160+
if rewritten.base.data.absoluteRaw.info.nodeId != info.nodeId {
161+
// The node was rewritten, let's handle it
162+
if newLayout == nil {
163+
// We have not yet collected any previous rewritten nodes. Initialize
164+
// the new layout with the previous nodes of the parent. This is
165+
// possible, since we know they were not rewritten.
166+
167+
// The below implementation is based on Collection.map but directly
168+
// reserves enough capacity for the entire layout.
169+
newLayout = ContiguousArray<RawSyntax?>()
170+
newLayout!.reserveCapacity(node.raw.numberOfChildren)
171+
for j in 0..<i {
172+
newLayout!.append(node.raw.child(at: j))
173+
}
174+
}
175+
176+
// Now that we know we have a new layout in which we collect rewritten
177+
// nodes, add it.
178+
newLayout!.append(rewritten.raw)
179+
} else {
180+
// The node was not changed by the rewriter. Only store it if a previous
181+
// node has been rewritten and we are collecting a rewritten layout.
182+
if newLayout != nil {
183+
newLayout!.append(raw)
184+
}
185+
}
92186
}
93187

94-
// Sanity check, ensure the new children are the same length.
95-
assert(newLayout.count == node.raw.numberOfChildren)
188+
if let newLayout = newLayout {
189+
// A child node was rewritten. Build the updated node.
190+
191+
// Sanity check, ensure the new children are the same length.
192+
assert(newLayout.count == node.raw.numberOfChildren)
193+
194+
let newRaw = node.raw.replacingLayout(Array(newLayout))
195+
return makeSyntax(.forRoot(newRaw))
196+
} else {
197+
// No child node was rewritten. So no need to change this node as well.
198+
return nodeS
199+
}
96200

97-
let newRaw = node.raw.replacingLayout(newLayout)
98-
return makeSyntax(.forRoot(newRaw))
99201
}
100202
}
101203

Tests/SwiftSyntaxTest/AbsolutePosition.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import XCTest
22
import SwiftSyntax
33

44
fileprivate class FuncRenamer: SyntaxRewriter {
5-
override func visit(_ node: FunctionDeclSyntax) ->DeclSyntax {
5+
override func visit(_ node: FunctionDeclSyntax) -> DeclSyntax {
66
return (super.visit(node) as! FunctionDeclSyntax).withIdentifier(
77
SyntaxFactory.makeIdentifier("anotherName"))
88
}

0 commit comments

Comments
 (0)