Skip to content

Commit 6087e6b

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 6087e6b

File tree

2 files changed

+103
-80
lines changed

2 files changed

+103
-80
lines changed

Sources/SwiftParser/Recovery.swift

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

113175
while !self.at(.endOfFile) {
114176
if !shouldSkipOverNewlines, self.atStartOfLine {
115177
break
116178
}
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(
179+
if let (matchedSpec, tokenSpec) = matchesSpec(&self) {
180+
let handle = RecoveryConsumptionHandle(
130181
unexpectedTokens: self.tokensConsumed - initialTokensConsumed,
131-
tokenConsumptionHandle: TokenConsumptionHandle(spec: matchedSpec)
182+
tokenConsumptionHandle: TokenConsumptionHandle(spec: tokenSpec)
132183
)
184+
return (matchedSpec, handle)
133185
}
134186
let currentTokenPrecedence = TokenPrecedence(self.currentToken)
135187
if currentTokenPrecedence >= recoveryPrecedence {
@@ -167,59 +219,4 @@ extension Parser.Lookahead {
167219

168220
return nil
169221
}
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-
}
225222
}

Tests/SwiftParserTest/DeclarationTests.swift

Lines changed: 26 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() {
@@ -1315,6 +1332,15 @@ final class DeclarationTests: ParserTestCase {
13151332
}
13161333
"""
13171334
)
1335+
1336+
assertParse(
1337+
"""
1338+
public var foo: Swift.Int {
1339+
get
1340+
@inlinable set {}
1341+
}
1342+
"""
1343+
)
13181344
}
13191345

13201346
func testInitAccessor() {

0 commit comments

Comments
 (0)