Skip to content

Commit 7251727

Browse files
authored
Merge pull request #205 from ahoppen/circumvent-stack-overflow
Resolve stack overflow in tree visitation when having a reduced stack size
2 parents a77fa8a + a0fd756 commit 7251727

File tree

2 files changed

+604
-32
lines changed

2 files changed

+604
-32
lines changed

Sources/SwiftSyntax/SyntaxRewriter.swift.gyb

Lines changed: 68 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -106,33 +106,85 @@ open class SyntaxRewriter {
106106

107107
% end
108108

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+
// SwiftSyntax requires a lot of stack space in debug builds for syntax tree
130+
// rewriting. In scenarios with reduced stack space (in particular dispatch
131+
// queues), this easily results in a stack overflow. To work around this issue,
132+
// use a less performant but also less stack-hungry version of SwiftSyntax's
133+
// SyntaxRewriter in debug builds.
134+
#if DEBUG
135+
136+
/// Implementation detail of visit(_:). Do not call directly.
137+
///
138+
/// Returns the function that shall be called to visit a specific syntax node.
139+
///
140+
/// To determine the correct specific visitation function for a syntax node,
141+
/// we need to switch through a huge switch statement that covers all syntax
142+
/// types. In debug builds, the cases of this switch statement do not share
143+
/// stack space (rdar://55929175). Because of this, the switch statement
144+
/// requires allocates about 15KB of stack space. In scenarios with reduced
145+
/// stack size (in particular dispatch queues), this often results in a stack
146+
/// overflow during syntax tree rewriting.
147+
///
148+
/// To circumvent this problem, make calling the specific visitation function
149+
/// a two-step process: First determine the function to call in this function
150+
/// and return a reference to it, then call it. This way, the stack frame
151+
/// that determines the correct visitiation function will be popped of the
152+
/// stack before the function is being called, making the switch's stack
153+
/// space transient instead of having it linger in the call stack.
154+
private func visitationFunc(for data: SyntaxData) -> ((SyntaxData) -> Syntax) {
155+
switch data.raw.kind {
156+
case .token:
157+
return visitImplTokenSyntax
158+
case .unknown:
159+
return visitImplUnknownSyntax
160+
% for node in SYNTAX_NODES:
161+
case .${node.swift_syntax_kind}:
162+
return visitImpl${node.name}
163+
% end
164+
}
165+
}
166+
167+
private func visit(_ data: SyntaxData) -> Syntax {
168+
return visitationFunc(for: data)(data)
169+
}
170+
171+
#else
172+
109173
private func visit(_ data: SyntaxData) -> Syntax {
110174
switch data.raw.kind {
111175
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)
176+
return visitImplTokenSyntax(data)
118177
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
178+
return visitImplUnknownSyntax(data)
129179
% for node in SYNTAX_NODES:
130180
case .${node.swift_syntax_kind}:
131181
return visitImpl${node.name}(data)
132182
% end
133183
}
134184
}
135185

186+
#endif
187+
136188
private func visitChildren<SyntaxType: SyntaxProtocol>(
137189
_ node: SyntaxType
138190
) -> SyntaxType {

0 commit comments

Comments
 (0)