8000 Add workaround for stack overflow in tree visitation when having a re… · swiftlang/swift-syntax@f622dcc · GitHub
[go: up one dir, main page]

Skip to content

Commit f622dcc

Browse files
ahoppenakyrtzi
authored andcommitted
Add workaround for 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 adds a flag that 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. The workaround currently has a 50% performance decrease in release builds, so it is only used in debug builds.
1 parent 7591465 commit f622dcc

File tree

2 files changed

+600
-32
lines changed

2 files changed

+600
-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)
0