Skip to content

Commit cfe2a11

Browse files
committed
Use two-step dispatch to SyntaxVisitor.vistImpl
`SyntaxRewriter` has had a problem with stack allocation space for a while because cases in a switch statement don’t share stack space in debug builds (rdar://55929175). We started seeing this issue with `SyntaxVisitor` as well now. Use the same approach to fix the problem: To circumvent this problem, make calling the specific visitation function a two-step process: First determine the function to call in this function and return a reference to it, then call it. This way, the stack frame that determines the correct visitation 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. And to make the entire approach look sane, allow 4 arguments on the same line for SyntaxVisitor instead of the 3 that we allow in all other files.
1 parent 0d6a210 commit cfe2a11

File tree

6 files changed

+1529
-1679
lines changed

6 files changed

+1529
-1679
lines changed

CodeGeneration/Sources/Utils/CodeGenerationFormat.swift

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,12 @@ import SwiftSyntax
1515

1616
/// A format style for files generated by CodeGeneration.
1717
public class CodeGenerationFormat: BasicFormat {
18-
public init() {
18+
/// The maximum number of array/dictionary/function parameters/tuple elements
19+
/// that should be put on the same line.
20+
private let maxElementsOnSameLine: Int
21+
22+
public init(maxElementsOnSameLine: Int = 3) {
23+
self.maxElementsOnSameLine = maxElementsOnSameLine
1924
super.init(indentationWidth: .spaces(2))
2025
}
2126

@@ -26,7 +31,7 @@ public class CodeGenerationFormat: BasicFormat {
2631
public override func visit(_ node: ArrayElementListSyntax) -> ArrayElementListSyntax {
2732
let children = node.children(viewMode: .all)
2833
// Short array literals are presented on one line, list each element on a different line.
29-
if children.count > 3 {
34+
if children.count > maxElementsOnSameLine {
3035
return ArrayElementListSyntax(formatChildrenSeparatedByNewline(children: children, elementType: ArrayElementSyntax.self))
3136
} else {
3237
return super.visit(node)
@@ -45,7 +50,7 @@ public class CodeGenerationFormat: BasicFormat {
4550
public override func visit(_ node: DictionaryElementListSyntax) -> DictionaryElementListSyntax {
4651
let children = node.children(viewMode: .all)
4752
// Short dictionary literals are presented on one line, list each element on a different line.
48-
if children.count > 3 {
53+
if children.count > maxElementsOnSameLine {
4954
return DictionaryElementListSyntax(formatChildrenSeparatedByNewline(children: children, elementType: DictionaryElementSyntax.self))
5055
} else {
5156
return super.visit(node)
@@ -55,7 +60,7 @@ public class CodeGenerationFormat: BasicFormat {
5560
public override func visit(_ node: FunctionParameterListSyntax) -> FunctionParameterListSyntax {
5661
let children = node.children(viewMode: .all)
5762
// Short function parameter literals are presented on one line, list each element on a different line.
58-
if children.count > 3 {
63+
if children.count > maxElementsOnSameLine {
5964
return FunctionParameterListSyntax(formatChildrenSeparatedByNewline(children: children, elementType: FunctionParameterSyntax.self))
6065
} else {
6166
return super.visit(node)
@@ -82,7 +87,7 @@ public class CodeGenerationFormat: BasicFormat {
8287
public override func visit(_ node: LabeledExprListSyntax) -> LabeledExprListSyntax {
8388
let children = node.children(viewMode: .all)
8489
// Short tuple element list literals are presented on one line, list each element on a different line.
85-
if children.count > 3 {
90+
if children.count > maxElementsOnSameLine {
8691
return LabeledExprListSyntax(formatChildrenSeparatedByNewline(children: children, elementType: LabeledExprSyntax.self))
8792
} else {
8893
return super.visit(node)

CodeGeneration/Sources/generate-swift-syntax/GenerateSwiftSyntax.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,8 @@ struct GeneratedFileSpec {
4747
self.contentsGenerator = contents
4848
}
4949

50-
init(_ pathComponents: [String], _ contents: @escaping @autoclosure () -> SourceFileSyntax) {
51-
self.init(pathComponents, "\(contents().formatted(using: CodeGenerationFormat()))\n")
50+
init(_ pathComponents: [String], _ contents: @escaping @autoclosure () -> SourceFileSyntax, format: CodeGenerationFormat = CodeGenerationFormat()) {
51+
self.init(pathComponents, "\(contents().formatted(using: format))\n")
5252
}
5353
}
5454

@@ -112,7 +112,7 @@ struct GenerateSwiftSyntax: ParsableCommand {
112112
GeneratedFileSpec(swiftSyntaxGeneratedDir + ["SyntaxRewriter.swift"], syntaxRewriterFile),
113113
GeneratedFileSpec(swiftSyntaxGeneratedDir + ["SyntaxTraits.swift"], syntaxTraitsFile),
114114
GeneratedFileSpec(swiftSyntaxGeneratedDir + ["SyntaxTransform.swift"], syntaxTransformFile),
115-
GeneratedFileSpec(swiftSyntaxGeneratedDir + ["SyntaxVisitor.swift"], syntaxVisitorFile),
115+
GeneratedFileSpec(swiftSyntaxGeneratedDir + ["SyntaxVisitor.swift"], syntaxVisitorFile, format: CodeGenerationFormat(maxElementsOnSameLine: 4)),
116116
GeneratedFileSpec(swiftSyntaxGeneratedDir + ["TokenKind.swift"], tokenKindFile),
117117
GeneratedFileSpec(swiftSyntaxGeneratedDir + ["Tokens.swift"], tokensFile),
118118
GeneratedFileSpec(swiftSyntaxGeneratedDir + ["TriviaPieces.swift"], triviaPiecesFile),

CodeGeneration/Sources/generate-swift-syntax/templates/swiftsyntax/SyntaxRewriterFile.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ let syntaxRewriterFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
210210
/// we need to switch through a huge switch statement that covers all syntax
211211
/// types. In debug builds, the cases of this switch statement do not share
212212
/// stack space (rdar://55929175). Because of this, the switch statement
213-
/// requires allocates about 15KB of stack space. In scenarios with reduced
213+
/// requires about 15KB of stack space. In scenarios with reduced
214214
/// stack size (in particular dispatch queues), this often results in a stack
215215
/// overflow during syntax tree rewriting.
216216
///

CodeGeneration/Sources/generate-swift-syntax/templates/swiftsyntax/SyntaxVisitorFile.swift

Lines changed: 99 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -113,27 +113,108 @@ let syntaxVisitorFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
113113
"""
114114
)
115115

116-
try FunctionDeclSyntax("private func visit(_ data: SyntaxData)") {
117-
try SwitchExprSyntax("switch data.raw.kind") {
118-
SwitchCaseSyntax("case .token:") {
119-
DeclSyntax("let node = TokenSyntax(data)")
120-
121-
ExprSyntax("_ = visit(node)")
122-
ExprSyntax(
123-
"""
124-
// No children to visit.
125-
visitPost(node)
126-
"""
116+
try IfConfigDeclSyntax(
117+
leadingTrivia:
118+
"""
119+
// SwiftSyntax requires a lot of stack space in debug builds for syntax tree
120+
// visitation. In scenarios with reduced stack space (in particular dispatch
121+
// queues), this easily results in a stack overflow. To work around this issue,
122+
// use a less performant but also less stack-hungry version of SwiftSyntax's
123+
// SyntaxVisitor in debug builds.
124+
125+
""",
126+
clauses: IfConfigClauseListSyntax {
127+
IfConfigClauseSyntax(
128+
poundKeyword: .poundIfToken(),
129+
condition: ExprSyntax("DEBUG"),
130+
elements: .statements(
131+
try CodeBlockItemListSyntax {
132+
try FunctionDeclSyntax(
133+
"""
134+
/// Implementation detail of visit(_:). Do not call directly.
135+
///
136+
/// Returns the function that shall be called to visit a specific syntax node.
137+
///
138+
/// To determine the correct specific visitation function for a syntax node,
139+
/// we need to switch through a huge switch statement that covers all syntax
140+
/// types. In debug builds, the cases of this switch statement do not share
141+
/// stack space (rdar://55929175). Because of this, the switch statement
142+
/// requires about 15KB of stack space. In scenarios with reduced
143+
/// stack size (in particular dispatch queues), this often results in a stack
144+
/// overflow during syntax tree rewriting.
145+
///
146+
/// To circumvent this problem, make calling the specific visitation function
147+
/// a two-step process: First determine the function to call in this function
148+
/// and return a reference to it, then call it. This way, the stack frame
149+
/// that determines the correct visitation function will be popped of the
150+
/// stack before the function is being called, making the switch's stack
151+
/// space transient instead of having it linger in the call stack.
152+
private func visitationFunc(for data: SyntaxData) -> ((SyntaxData) -> Void)
153+
"""
154+
) {
155+
try SwitchExprSyntax("switch data.raw.kind") {
156+
SwitchCaseSyntax("case .token:") {
157+
StmtSyntax(
158+
"""
159+
return {
160+
let node = TokenSyntax($0)
161+
_ = self.visit(node)
162+
// No children to visit.
163+
self.visitPost(node)
164+
}
165+
"""
166+
)
167+
}
168+
169+
for node in NON_BASE_SYNTAX_NODES {
170+
SwitchCaseSyntax("case .\(node.varOrCaseName):") {
171+
StmtSyntax("return { self.visitImpl($0, \(node.kind.syntaxType).self, self.visit, self.visitPost) }")
172+
}
173+
}
174+
}
175+
}
176+
177+
DeclSyntax(
178+
"""
179+
private func visit(_ data: SyntaxData) {
180+
return visitationFunc(for: data)(data)
181+
}
182+
"""
183+
)
184+
}
127185
)
128-
}
186+
)
187+
IfConfigClauseSyntax(
188+
poundKeyword: .poundElseToken(),
189+
elements: .statements(
190+
CodeBlockItemListSyntax {
191+
try! FunctionDeclSyntax("private func visit(_ data: SyntaxData)") {
192+
try SwitchExprSyntax("switch data.raw.kind") {
193+
SwitchCaseSyntax("case .token:") {
194+
DeclSyntax("let node = TokenSyntax(data)")
129195

130-
for node in NON_BASE_SYNTAX_NODES {
131-
SwitchCaseSyntax("case .\(node.varOrCaseName):") {
132-
ExprSyntax("visitImpl(data, \(node.kind.syntaxType).self, visit, visitPost)")
133-
}
134-
}
196+
ExprSyntax("_ = visit(node)")
197+
ExprSyntax(
198+
"""
199+
// No children to visit.
200+
visitPost(node)
201+
"""
202+
)
203+
}
204+
205+
for node in NON_BASE_SYNTAX_NODES {
206+
SwitchCaseSyntax("case .\(node.varOrCaseName):") {
207+
ExprSyntax("visitImpl(data, \(node.kind.syntaxType).self, visit, visitPost)")
208+
}
209+
}
210+
}
211+
}
212+
213+
}
214+
)
215+
)
135216
}
136-
}
217+
)
137218

138219
DeclSyntax(
139220
"""

Sources/SwiftSyntax/generated/SyntaxRewriter.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2073,7 +2073,7 @@ open class SyntaxRewriter {
20732073
/// we need to switch through a huge switch statement that covers all syntax
20742074
/// types. In debug builds, the cases of this switch statement do not share
20752075
/// stack space (rdar://55929175). Because of this, the switch statement
2076-
/// requires allocates about 15KB of stack space. In scenarios with reduced
2076+
/// requires about 15KB of stack space. In scenarios with reduced
20772077
/// stack size (in particular dispatch queues), this often results in a stack
20782078
/// overflow during syntax tree rewriting.
20792079
///

0 commit comments

Comments
 (0)