Skip to content

Commit 06b8c02

Browse files
committed
[Parser] Consolidate recovery codepaths
We had two different codepaths for recovery, which could result in cases where we attempt to recover, but `expect` is then not able to do so. Consolidate them into a single function.
1 parent 7392a3c commit 06b8c02

File tree

2 files changed

+96
-80
lines changed

2 files changed

+96
-80
lines changed

Sources/SwiftParser/Recovery.swift

Lines changed: 79 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -89,83 +89,34 @@ extension Parser.Lookahead {
8989
_ spec3: TokenSpec,
9090
recursionDepth: Int = 1
9191
) -> RecoveryConsumptionHandle? {
92-
if recursionDepth > 10 {
93-
// `canRecoverTo` calls itself recursively if it finds a nested opening token, eg. when calling `canRecoverTo` on
94-
// `{{{`. To avoid stack overflowing, limit the number of nested `canRecoverTo` calls we make. Since returning a
95-
// recovery handle from this function only improves error recovery but is not necessary for correctness, bailing
96-
// from recovery is safe.
97-
// The value 10 was chosen fairly arbitrarily. It seems unlikely that we get useful recovery if we find more than
98-
// 10 nested open and closing delimiters.
99-
return nil
100-
}
10192
#if SWIFTPARSER_ENABLE_ALTERNATE_TOKEN_INTROSPECTION
10293
if shouldRecordAlternativeTokenChoices {
10394
recordAlternativeTokenChoice(for: self.currentToken, choices: [spec1, spec2, spec3])
10495
}
10596
#endif
106-
let initialTokensConsumed = self.tokensConsumed
10797

108-
let recoveryPrecedence = min(spec1.recoveryPrecedence, spec2.recoveryPrecedence, spec3.recoveryPrecedence)
109-
let shouldSkipOverNewlines =
110-
recoveryPrecedence.shouldSkipOverNewlines && spec1.allowAtStartOfLine && spec2.allowAtStartOfLine
111-
&& spec3.allowAtStartOfLine
112-
113-
while !self.at(.endOfFile) {
114-
if !shouldSkipOverNewlines, self.atStartOfLine {
115-
break
116-
}
117-
let matchedSpec: TokenSpec?
118-
switch self.currentToken {
119-
case spec1:
120-
matchedSpec = spec1
121-
case spec2:
122-
matchedSpec = spec2
123-
case spec3:
124-
matchedSpec = spec3
125-
default:
126-
matchedSpec = nil
127-
}
128-
if let matchedSpec {
129-
return RecoveryConsumptionHandle(
130-
unexpectedTokens: self.tokensConsumed - initialTokensConsumed,
131-
tokenConsumptionHandle: TokenConsumptionHandle(spec: matchedSpec)
132-
)
133-
}
134-
let currentTokenPrecedence = TokenPrecedence(self.currentToken)
135-
if currentTokenPrecedence >= recoveryPrecedence {
136-
break
137-
}
138-
if let closingDelimiter = currentTokenPrecedence.closingTokenKind {
139-
let closingDelimiterSpec = TokenSpec(closingDelimiter)
140-
let canCloseAtSameLine: Int? = self.withLookahead { lookahead in
141-
var tokensToSkip = 0
142-
while !lookahead.at(.endOfFile), !lookahead.currentToken.isAtStartOfLine {
143-
tokensToSkip += 1
144-
if lookahead.at(closingDelimiterSpec) {
145-
return tokensToSkip
146-
} else {
147-
lookahead.consumeAnyToken()
148-
}
98+
let result = canRecoverToImpl(
99+
recoveryPrecedence: min(spec1.recoveryPrecedence, spec2.recoveryPrecedence, spec3.recoveryPrecedence),
100+
recursionDepth: recursionDepth,
101+
matchesSpec: { lookahead -> (TokenSpec, _)? in
102+
let match: TokenSpec? =
103+
switch lookahead.currentToken {
104+
case spec1:
105+
spec1
106+
case spec2:
107+
spec2
108+
case spec3:
109+
spec3
110+
default:
111+
nil
149112
}
113+
guard let match, match.allowAtStartOfLine || !lookahead.atStartOfLine else {
150114
return nil
151115
}
152-
if let tokensToSkip = canCloseAtSameLine {
153-
for _ in 0..<tokensToSkip {
154-
self.consumeAnyToken()
155-
}
156-
continue
157-
}
158-
self.consumeAnyToken()
159-
guard self.canRecoverTo(closingDelimiterSpec, recursionDepth: recursionDepth + 1) != nil else {
160-
continue
161-
}
162-
self.eat(closingDelimiterSpec)
163-
} else {
164-
self.consumeAnyToken()
116+
return (match, TokenConsumptionHandle(spec: match))
165117
}
166-
}
167-
168-
return nil
118+
)
119+
return result?.handle
169120
}
170121

171122
/// Checks if we can reach a token in `subset` by skipping tokens that have
@@ -182,7 +133,6 @@ extension Parser.Lookahead {
182133
recordAlternativeTokenChoice(for: self.currentToken, choices: specSet.allCases.map(\.spec))
183134
}
184135
#endif
185-
let initialTokensConsumed = self.tokensConsumed
186136

187137
if specSet.allCases.isEmpty {
188138
return nil
@@ -192,31 +142,80 @@ extension Parser.Lookahead {
192142
overrideRecoveryPrecedence ?? specSet.allCases.map({
193143
return $0.spec.recoveryPrecedence
194144
}).min()!
195-
var loopProgress = LoopProgressCondition()
196-
while !self.at(.endOfFile) && self.hasProgressed(&loopProgress) {
145+
146+
return self.canRecoverToImpl(
147+
recoveryPrecedence: recoveryPrecedence,
148+
recursionDepth: 1,
149+
matchesSpec: { lookahead in
150+
guard let (specSet, handle) = lookahead.at(anyIn: specSet),
151+
specSet.spec.allowAtStartOfLine || !lookahead.atStartOfLine
152+
else {
153+
return nil
154+
}
155+
return (specSet, handle)
156+
}
157+
)
158+
}
159+
160+
@inline(__always)
161+
private mutating func canRecoverToImpl<Match>(
162+
recoveryPrecedence: TokenPrecedence,
163+
recursionDepth: Int,
164+
matchesSpec: (Parser.Lookahead) -> (Match, TokenConsumptionHandle)?
165+
) -> (match: Match, handle: RecoveryConsumptionHandle)? {
166+
if recursionDepth > 10 {
167+
// `canRecoverToImpl` calls itself recursively if it finds a nested opening token, eg. when calling `canRecoverTo` on
168+
// `{{{`. To avoid stack overflowing, limit the number of nested `canRecoverTo` calls we make. Since returning a
169+
// recovery handle from this function only improves error recovery but is not necessary for correctness, bailing
170+
// from recovery is safe.
171+
// The value 10 was chosen fairly arbitrarily. It seems unlikely that we get useful recovery if we find more than
172+
// 10 nested open and closing delimiters.
173+
return nil
174+
}
175+
let initialTokensConsumed = self.tokensConsumed
176+
177+
while !self.at(.endOfFile) {
197178
if !recoveryPrecedence.shouldSkipOverNewlines, self.atStartOfLine {
198179
break
199180
}
200-
if let (kind, handle) = self.at(anyIn: specSet) {
201-
return (
202-
kind,
203-
RecoveryConsumptionHandle(
204-
unexpectedTokens: self.tokensConsumed - initialTokensConsumed,
205-
tokenConsumptionHandle: handle
206-
)
181+
if let (matchedSpec, tokenHandle) = matchesSpec(self) {
182+
let handle = RecoveryConsumptionHandle(
183+
unexpectedTokens: self.tokensConsumed - initialTokensConsumed,
184+
tokenConsumptionHandle: tokenHandle
207185
)
186+
return (matchedSpec, handle)
208187
}
209188
let currentTokenPrecedence = TokenPrecedence(self.currentToken)
210189
if currentTokenPrecedence >= recoveryPrecedence {
211190
break
212191
}
213-
self.consumeAnyToken()
214192
if let closingDelimiter = currentTokenPrecedence.closingTokenKind {
215193
let closingDelimiterSpec = TokenSpec(closingDelimiter)
216-
guard self.canRecoverTo(closingDelimiterSpec) != nil else {
217-
break
194+
let canCloseAtSameLine: Int? = self.withLookahead { lookahead in
195+
var tokensToSkip = 0
196+
while !lookahead.at(.endOfFile), !lookahead.currentToken.isAtStartOfLine {
197+
tokensToSkip += 1
198+
if lookahead.at(closingDelimiterSpec) {
199+
return tokensToSkip
200+
} else {
201+
lookahead.consumeAnyToken()
202+
}
203+
}
204+
return nil
205+
}
206+
if let tokensToSkip = canCloseAtSameLine {
207+
for _ in 0..<tokensToSkip {
208+
self.consumeAnyToken()
209+
}
210+
continue
211+
}
212+
self.consumeAnyToken()
213+
guard self.canRecoverTo(closingDelimiterSpec, recursionDepth: recursionDepth + 1) != nil else {
214+
continue
218215
}
219216
self.eat(closingDelimiterSpec)
217+
} else {
218+
self.consumeAnyToken()
220219
}
221220
}
222221

Tests/SwiftParserTest/DeclarationTests.swift

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -617,6 +617,23 @@ final class DeclarationTests: ParserTestCase {
617617
private(set) var get, didSet var a = 0
618618
"""
619619
)
620+
621+
assertParse(
622+
"""
623+
public 1️⃣{ {} }
624+
open
625+
""",
626+
diagnostics: [
627+
DiagnosticSpec(
628+
message: "expected declaration and ';' after 'public' modifier",
629+
fixIts: ["insert declaration and ';'"]
630+
)
631+
],
632+
fixedSource: """
633+
public <#declaration#>; { {} }
634+
open
635+
"""
636+
)
620637
}
621638

622639
func testTypealias() {

0 commit comments

Comments
 (0)