Skip to content

Commit 3208c73

Browse files
committed
[NFC] Reduce SwiftParser stack usage in debug mode
I’ve had trouble running SwiftParser’s tests—particularly the torture tests in the swiftlang/swift repo—locally in debug mode because some common parser functions would use many kilobytes of stack for each call, consuming the stack too quickly for the `maximumNestingLevel` to control. Break up a number of problematic functions to help with this, and also insert a missing stack overflow check in `parseClosureExpression()`.
1 parent 8ea19b6 commit 3208c73

File tree

5 files changed

+1131
-849
lines changed

5 files changed

+1131
-849
lines changed

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

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -40,20 +40,27 @@ let keywordFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
4040
try! SwitchExprSyntax("switch text.count") {
4141
for (length, keywords) in keywordsByLength() {
4242
SwitchCaseSyntax("case \(raw: length):") {
43-
try! SwitchExprSyntax("switch text") {
44-
for keyword in keywords {
45-
SwitchCaseSyntax("case \(literal: keyword.name):") {
46-
ExprSyntax("self = .\(keyword.enumCaseCallName)")
47-
}
48-
}
49-
SwitchCaseSyntax("default: return nil")
50-
}
43+
ExprSyntax("self.init(_length\(raw: length): text)")
5144
}
5245
}
5346
SwitchCaseSyntax("default: return nil")
5447
}
5548
}
5649

50+
// Split into individual initializers by length to reduce stack use
51+
for (length, keywords) in keywordsByLength() {
52+
try! InitializerDeclSyntax("private init?(_length\(raw: length) text: SyntaxText)") {
53+
try! SwitchExprSyntax("switch text") {
54+
for keyword in keywords {
55+
SwitchCaseSyntax("case \(literal: keyword.name):") {
56+
ExprSyntax("self = .\(keyword.enumCaseCallName)")
57+
}
58+
}
59+
SwitchCaseSyntax("default: return nil")
60+
}
61+
}
62+
}
63+
5764
DeclSyntax(
5865
"""
5966
/// This is really unfortunate. Really, we should have a `switch` in

Sources/SwiftParser/Declarations.swift

Lines changed: 77 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -68,36 +68,7 @@ extension TokenConsumer {
6868
) -> Bool {
6969
var subparser = self.lookahead()
7070

71-
var hasAttribute = false
72-
var attributeProgress = LoopProgressCondition()
73-
while subparser.hasProgressed(&attributeProgress) {
74-
if subparser.at(.atSign) {
75-
_ = subparser.consumeAttributeList()
76-
hasAttribute = true
77-
} else if subparser.at(.poundIf) && subparser.consumeIfConfigOfAttributes() {
78-
hasAttribute = true
79-
} else {
80-
break
81-
}
82-
}
83-
84-
var hasModifier = false
85-
if subparser.currentToken.isLexerClassifiedKeyword || subparser.currentToken.rawTokenKind == .identifier {
86-
var modifierProgress = LoopProgressCondition()
87-
while let (modifierKind, handle) = subparser.at(anyIn: DeclarationModifier.self),
88-
modifierKind != .class,
89-
subparser.hasProgressed(&modifierProgress)
90-
{
91-
hasModifier = true
92-
subparser.eat(handle)
93-
if modifierKind != .open && subparser.at(.leftParen) && modifierKind.canHaveParenthesizedArgument {
94-
// When determining whether we are at a declaration, don't consume anything in parentheses after 'open'
95-
// so we don't consider a function call to open as a decl modifier. This matches the C++ parser.
96-
subparser.consumeAnyToken()
97-
subparser.consume(to: .rightParen)
98-
}
99-
}
100-
}
71+
let (hasAttribute, hasModifier) = subparser.skipAttributesAndModifiers()
10172

10273
if hasAttribute {
10374
if subparser.at(.rightBrace) || subparser.at(.endOfFile) || subparser.at(.poundEndif) {
@@ -112,17 +83,7 @@ extension TokenConsumer {
11283
switch subparser.at(anyIn: DeclarationKeyword.self)?.0 {
11384
case .lhs(.actor):
11485
// actor Foo {}
115-
if subparser.peek().rawTokenKind == .identifier {
116-
return true
117-
}
118-
// actor may be somewhere in the modifier list. Eat the tokens until we get
119-
// to something that isn't the start of a decl. If that is an identifier,
120-
// it's an actor declaration, otherwise, it isn't.
121-
var lookahead = subparser.lookahead()
122-
repeat {
123-
lookahead.consumeAnyToken()
124-
} while lookahead.atStartOfDeclaration(allowInitDecl: allowInitDecl, requiresDecl: requiresDecl)
125-
return lookahead.at(.identifier)
86+
return subparser.atStartOfActor(allowInitDecl: allowInitDecl, requiresDecl: requiresDecl)
12687
case .lhs(.case):
12788
// When 'case' appears inside a function, it's probably a switch
12889
// case, not an enum case declaration.
@@ -146,23 +107,7 @@ extension TokenConsumer {
146107
return false
147108
}
148109

149-
var lookahead = subparser.lookahead()
150-
151-
// Consume 'using'
152-
lookahead.consumeAnyToken()
153-
154-
// Allow parsing 'using' as declaration only if
155-
// it's immediately followed by either `@` or
156-
// an identifier.
157-
if lookahead.atStartOfLine {
158-
return false
159-
}
160-
161-
guard lookahead.at(.atSign) || lookahead.at(.identifier) else {
162-
return false
163-
}
164-
165-
return true
110+
return subparser.atStartOfUsing()
166111
case .some(_):
167112
// All other decl start keywords unconditionally start a decl.
168113
return true
@@ -200,6 +145,80 @@ extension TokenConsumer {
200145
}
201146
}
202147

148+
extension Parser.Lookahead {
149+
fileprivate mutating func skipAttributesAndModifiers() -> (hasAttribute: Bool, hasModifier: Bool) {
150+
var hasAttribute = false
151+
var attributeProgress = LoopProgressCondition()
152+
while self.hasProgressed(&attributeProgress) {
153+
if self.at(.atSign) {
154+
_ = self.consumeAttributeList()
155+
hasAttribute = true
156+
} else if self.at(.poundIf) && self.consumeIfConfigOfAttributes() {
157+
hasAttribute = true
158+
} else {
159+
break
160+
}
161+
}
162+
163+
var hasModifier = false
164+
if self.currentToken.isLexerClassifiedKeyword || self.currentToken.rawTokenKind == .identifier {
165+
var modifierProgress = LoopProgressCondition()
166+
while let (modifierKind, handle) = self.at(anyIn: DeclarationModifier.self),
167+
modifierKind != .class,
168+
self.hasProgressed(&modifierProgress)
169+
{
170+
hasModifier = true
171+
self.eat(handle)
172+
if modifierKind != .open && self.at(.leftParen) && modifierKind.canHaveParenthesizedArgument {
173+
// When determining whether we are at a declaration, don't consume anything in parentheses after 'open'
174+
// so we don't consider a function call to open as a decl modifier. This matches the C++ parser.
175+
self.consumeAnyToken()
176+
self.consume(to: .rightParen)
177+
}
178+
}
179+
}
180+
181+
return (hasAttribute, hasModifier)
182+
}
183+
184+
fileprivate mutating func atStartOfActor(
185+
allowInitDecl: Bool,
186+
requiresDecl: Bool
187+
) -> Bool {
188+
if self.peek().rawTokenKind == .identifier {
189+
return true
190+
}
191+
// actor may be somewhere in the modifier list. Eat the tokens until we get
192+
// to something that isn't the start of a decl. If that is an identifier,
193+
// it's an actor declaration, otherwise, it isn't.
194+
var lookahead = self.lookahead()
195+
repeat {
196+
lookahead.consumeAnyToken()
197+
} while lookahead.atStartOfDeclaration(allowInitDecl: allowInitDecl, requiresDecl: requiresDecl)
198+
return lookahead.at(.identifier)
199+
}
200+
201+
fileprivate mutating func atStartOfUsing() -> Bool {
202+
var lookahead = self.lookahead()
203+
204+
// Consume 'using'
205+
lookahead.consumeAnyToken()
206+
207+
// Allow parsing 'using' as declaration only if
208+
// it's immediately followed by either `@` or
209+
// an identifier.
210+
if lookahead.atStartOfLine {
211+
return false
212+
}
213+
214+
guard lookahead.at(.atSign) || lookahead.at(.identifier) else {
215+
return false
216+
}
217+
218+
return true
219+
}
220+
}
221+
203222
extension Parser {
204223
struct DeclAttributes {
205224
var attributes: RawAttributeListSyntax

0 commit comments

Comments
 (0)