Skip to content

Commit e80d8e6

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 baef1ff commit e80d8e6

File tree

5 files changed

+1127
-850
lines changed

5 files changed

+1127
-850
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: 78 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -63,37 +63,7 @@ extension TokenConsumer {
6363

6464
var subparser = self.lookahead()
6565

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

9868
if hasAttribute {
9969
if subparser.at(.rightBrace) || subparser.at(.endOfFile) || subparser.at(.poundEndif) {
@@ -118,17 +88,7 @@ extension TokenConsumer {
11888
switch declStartKeyword {
11989
case .lhs(.actor):
12090
// actor Foo {}
121-
if subparser.peek().rawTokenKind == .identifier {
122-
return true
123-
}
124-
// actor may be somewhere in the modifier list. Eat the tokens until we get
125-
// to something that isn't the start of a decl. If that is an identifier,
126-
// it's an actor declaration, otherwise, it isn't.
127-
var lookahead = subparser.lookahead()
128-
repeat {
129-
lookahead.consumeAnyToken()
130-
} while lookahead.atStartOfDeclaration(isAtTopLevel: isAtTopLevel, allowInitDecl: allowInitDecl)
131-
return lookahead.at(.identifier)
91+
return subparser.atStartOfActor(isAtTopLevel: isAtTopLevel, allowInitDecl: allowInitDecl)
13292
case .lhs(.case):
13393
// When 'case' appears inside a function, it's probably a switch
13494
// case, not an enum case declaration.
@@ -152,23 +112,7 @@ extension TokenConsumer {
152112
return false
153113
}
154114

155-
var lookahead = subparser.lookahead()
156-
157-
// Consume 'using'
158-
lookahead.consumeAnyToken()
159-
160-
// Allow parsing 'using' as declaration only if
161-
// it's immediately followed by either `@` or
162-
// an identifier.
163-
if lookahead.atStartOfLine {
164-
return false
165-
}
166-
167-
guard lookahead.at(.atSign) || lookahead.at(.identifier) else {
168-
return false
169-
}
170-
171-
return true
115+
return subparser.atStartOfUsing()
172116
case .some(_):
173117
// All other decl start keywords unconditionally start a decl.
174118
return true
@@ -190,6 +134,81 @@ extension TokenConsumer {
190134
}
191135
}
192136

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

0 commit comments

Comments
 (0)