Skip to content

Commit 2929238

Browse files
committed
Resolve stack overflow in tree visitation when having a reduced stack size
To determine the correct specific visitation function for a syntax node, we need to switch through a huge switch statement that covers all syntax types. In debug builds, the cases of this switch statement do not share stack space (rdar://55929175). Because of this, the switch statement requires allocates about 15KB of stack space. In scenarios with reduced stack size (in particular dispatch queues), this often results in a stack overflow during syntax tree visitation. To circumvent this problem, this commit moves the retrieval of the specific visitation function to its own function. This way, the stack frame that determines the correct visitiation function will be popped of the stack before the function is being called, making the switch's stack space transient instead of having it linger in the call stack.
1 parent a77fa8a commit 2929238

File tree

2 files changed

+323
-271
lines changed

2 files changed

+323
-271
lines changed

Sources/SwiftSyntax/SyntaxRewriter.swift.gyb

Lines changed: 47 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -106,33 +106,62 @@ open class SyntaxRewriter {
106106

107107
% end
108108

109-
private func visit(_ data: SyntaxData) -> Syntax {
109+
/// Implementation detail of visit(_:). Do not call directly.
110+
private func visitImplTokenSyntax(_ data: SyntaxData) -> Syntax {
111+
let node = TokenSyntax(data)
112+
// Accessing _syntaxNode directly is faster than calling Syntax(node)
113+
visitPre(node._syntaxNode)
114+
defer { visitPost(node._syntaxNode) }
115+
if let newNode = visitAny(node._syntaxNode) { return newNode }
116+
return visit(node)
117+
}
118+
119+
/// Implementation detail of visit(_:). Do not call directly.
120+
private func visitImplUnknownSyntax(_ data: SyntaxData) -> Syntax {
121+
let node = UnknownSyntax(data)
122+
// Accessing _syntaxNode directly is faster than calling Syntax(node)
123+
visitPre(node._syntaxNode)
124+
defer { visitPost(node._syntaxNode) }
125+
if let newNode = visitAny(node._syntaxNode) { return newNode }
126+
return visit(node)
127+
}
128+
129+
130+
/// Implementation detail of visit(_:). Do not call directly.
131+
///
132+
/// Returns the function that shall be called to visit a specific syntax node.
133+
///
134+
/// To determine the correct specific visitation function for a syntax node,
135+
/// we need to switch through a huge switch statement that covers all syntax
136+
/// types. In debug builds, the cases of this switch statement do not share
137+
/// stack space (rdar://55929175). Because of this, the switch statement
138+
/// requires allocates about 15KB of stack space. In scenarios with reduced
139+
/// stack size (in particular dispatch queues), this often results in a stack
140+
/// overflow during syntax tree visitation.
141+
///
142+
/// To circumvent this problem, make calling the specific visitation function
143+
/// a two-step process: First determine the function to call in this function
144+
/// and return a reference to it, then call it. This way, the stack frame
145+
/// that determines the correct visitiation function will be popped of the
146+
/// stack before the function is being called, making the switch's stack
147+
/// space transient instead of having it linger in the call stack.
148+
private func visitationFunc(for data: SyntaxData) -> ((SyntaxData) -> Syntax) {
110149
switch data.raw.kind {
111150
case .token:
112-
let node = TokenSyntax(data)
113-
// Accessing _syntaxNode directly is faster than calling Syntax(node)
114-
visitPre(node._syntaxNode)
115-
defer { visitPost(node._syntaxNode) }
116-
if let newNode = visitAny(node._syntaxNode) { return newNode }
117-
return visit(node)
151+
return visitImplTokenSyntax
118152
case .unknown:
119-
let node = UnknownSyntax(data)
120-
// Accessing _syntaxNode directly is faster than calling Syntax(node)
121-
visitPre(node._syntaxNode)
122-
defer { visitPost(node._syntaxNode) }
123-
if let newNode = visitAny(node._syntaxNode) { return newNode }
124-
return visit(node)
125-
// The implementation of every generated case goes into its own function. This
126-
// circumvents an issue where the compiler allocates stack space for every
127-
// case statement next to each other in debug builds, causing it to allocate
128-
// ~50KB per call to this function. rdar://55929175
153+
return visitImplUnknownSyntax
129154
% for node in SYNTAX_NODES:
130155
case .${node.swift_syntax_kind}:
131-
return visitImpl${node.name}(data)
156+
return visitImpl${node.name}
132157
% end
133158
}
134159
}
135160

161+
private func visit(_ data: SyntaxData) -> Syntax {
162+
return visitationFunc(for: data)(data)
163+
}
164+
136165
private func visitChildren<SyntaxType: SyntaxProtocol>(
137166
_ node: SyntaxType
138167
) -> SyntaxType {

0 commit comments

Comments
 (0)