Skip to content

Commit 69ef996

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 06f123a commit 69ef996

File tree

2 files changed

+92
-81
lines changed

2 files changed

+92
-81
lines changed

Sources/SwiftParser/Recovery.swift

Lines changed: 75 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -89,47 +89,96 @@ extension Parser.Lookahead {
8989
_ spec3: TokenSpec,
9090
recursionDepth: Int = 1
9191
) -> RecoveryConsumptionHandle? {
92+
#if SWIFTPARSER_ENABLE_ALTERNATE_TOKEN_INTROSPECTION
93+
if shouldRecordAlternativeTokenChoices {
94+
recordAlternativeTokenChoice(for: self.currentToken, choices: [spec1, spec2, spec3])
95+
}
96+
#endif
97+
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
112+
}
113+
guard let match else { return nil }
114+
return (match, match)
115+
}
116+
)
117+
return result?.handle
118+
}
119+
120+
/// Checks if we can reach a token in `subset` by skipping tokens that have
121+
/// a precedence that have a lower ``TokenPrecedence`` than the minimum
122+
/// precedence of a token in that subset.
123+
/// If so, return the token that we can recover to and a handle that can be
124+
/// used to consume the unexpected tokens and the token we recovered to.
125+
mutating func canRecoverTo<SpecSet: TokenSpecSet>(
126+
anyIn specSet: SpecSet.Type,
127+
overrideRecoveryPrecedence: TokenPrecedence? = nil
128+
) -> (match: SpecSet, handle: RecoveryConsumptionHandle)? {
129+
#if SWIFTPARSER_ENABLE_ALTERNATE_TOKEN_INTROSPECTION
130+
if shouldRecordAlternativeTokenChoices {
131+
recordAlternativeTokenChoice(for: self.currentToken, choices: specSet.allCases.map(\.spec))
132+
}
133+
#endif
134+
135+
if specSet.allCases.isEmpty {
136+
return nil
137+
}
138+
139+
let recoveryPrecedence =
140+
overrideRecoveryPrecedence ?? specSet.allCases.map({
141+
return $0.spec.recoveryPrecedence
142+
}).min()!
143+
144+
return self.canRecoverToImpl(
145+
recoveryPrecedence: recoveryPrecedence,
146+
recursionDepth: 1,
147+
matchesSpec: { lookahead in
148+
guard let (specSet, _) = lookahead.at(anyIn: specSet) else { return nil }
149+
return (specSet, specSet.spec)
150+
}
151+
)
152+
}
153+
154+
@inline(__always)
155+
private mutating func canRecoverToImpl<Match>(
156+
recoveryPrecedence: TokenPrecedence,
157+
recursionDepth: Int,
158+
matchesSpec: (inout Parser.Lookahead) -> (Match, TokenSpec)?
159+
) -> (match: Match, handle: RecoveryConsumptionHandle)? {
92160
if recursionDepth > 10 {
93-
// `canRecoverTo` calls itself recursively if it finds a nested opening token, eg. when calling `canRecoverTo` on
161+
// `canRecoverToImpl` calls itself recursively if it finds a nested opening token, eg. when calling `canRecoverTo` on
94162
// `{{{`. To avoid stack overflowing, limit the number of nested `canRecoverTo` calls we make. Since returning a
95163
// recovery handle from this function only improves error recovery but is not necessary for correctness, bailing
96164
// from recovery is safe.
97165
// The value 10 was chosen fairly arbitrarily. It seems unlikely that we get useful recovery if we find more than
98166
// 10 nested open and closing delimiters.
99167
return nil
100168
}
101-
#if SWIFTPARSER_ENABLE_ALTERNATE_TOKEN_INTROSPECTION
102-
if shouldRecordAlternativeTokenChoices {
103-
recordAlternativeTokenChoice(for: self.currentToken, choices: [spec1, spec2, spec3])
104-
}
105-
#endif
106169
let initialTokensConsumed = self.tokensConsumed
107170

108-
let recoveryPrecedence = min(spec1.recoveryPrecedence, spec2.recoveryPrecedence, spec3.recoveryPrecedence)
109-
let shouldSkipOverNewlines =
110-
recoveryPrecedence.shouldSkipOverNewlines && spec1.allowAtStartOfLine && spec2.allowAtStartOfLine
111-
&& spec3.allowAtStartOfLine
112-
113171
while !self.at(.endOfFile) {
114-
if !shouldSkipOverNewlines, self.atStartOfLine {
172+
if !recoveryPrecedence.shouldSkipOverNewlines, self.atStartOfLine {
115173
break
116174
}
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(
175+
if let (matchedSpec, tokenSpec) = matchesSpec(&self) {
176+
if !tokenSpec.allowAtStartOfLine && self.atStartOfLine { break }
177+
let handle = RecoveryConsumptionHandle(
130178
unexpectedTokens: self.tokensConsumed - initialTokensConsumed,
131-
tokenConsumptionHandle: TokenConsumptionHandle(spec: matchedSpec)
179+
tokenConsumptionHandle: TokenConsumptionHandle(spec: tokenSpec)
132180
)
181+
return (matchedSpec, handle)
133182
}
134183
let currentTokenPrecedence = TokenPrecedence(self.currentToken)
135184
if currentTokenPrecedence >= recoveryPrecedence {
@@ -167,59 +216,4 @@ extension Parser.Lookahead {
167216

168217
return nil
169218
}
170-
171-
/// Checks if we can reach a token in `subset` by skipping tokens that have
172-
/// a precedence that have a lower ``TokenPrecedence`` than the minimum
173-
/// precedence of a token in that subset.
174-
/// If so, return the token that we can recover to and a handle that can be
175-
/// used to consume the unexpected tokens and the token we recovered to.
176-
mutating func canRecoverTo<SpecSet: TokenSpecSet>(
177-
anyIn specSet: SpecSet.Type,
178-
overrideRecoveryPrecedence: TokenPrecedence? = nil
179-
) -> (match: SpecSet, handle: RecoveryConsumptionHandle)? {
180-
#if SWIFTPARSER_ENABLE_ALTERNATE_TOKEN_INTROSPECTION
181-
if shouldRecordAlternativeTokenChoices {
182-
recordAlternativeTokenChoice(for: self.currentToken, choices: specSet.allCases.map(\.spec))
183-
}
184-
#endif
185-
let initialTokensConsumed = self.tokensConsumed
186-
187-
if specSet.allCases.isEmpty {
188-
return nil
189-
}
190-
191-
let recoveryPrecedence =
192-
overrideRecoveryPrecedence ?? specSet.allCases.map({
193-
return $0.spec.recoveryPrecedence
194-
}).min()!
195-
var loopProgress = LoopProgressCondition()
196-
while !self.at(.endOfFile) && self.hasProgressed(&loopProgress) {
197-
if !recoveryPrecedence.shouldSkipOverNewlines, self.atStartOfLine {
198-
break
199-
}
200-
if let (kind, handle) = self.at(anyIn: specSet) {
201-
return (
202-
kind,
203-
RecoveryConsumptionHandle(
204-
unexpectedTokens: self.tokensConsumed - initialTokensConsumed,
205-
tokenConsumptionHandle: handle
206-
)
207-
)
208-
}
209-
let currentTokenPrecedence = TokenPrecedence(self.currentToken)
210-
if currentTokenPrecedence >= recoveryPrecedence {
211-
break
212-
}
213-
self.consumeAnyToken()
214-
if let closingDelimiter = currentTokenPrecedence.closingTokenKind {
215-
let closingDelimiterSpec = TokenSpec(closingDelimiter)
216-
guard self.canRecoverTo(closingDelimiterSpec) != nil else {
217-
break
218-
}
219-
self.eat(closingDelimiterSpec)
220-
}
221-
}
222-
223-
return nil
224-
}
225219
}

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)