Skip to content

Commit 00cf557

Browse files
committed
Support recovery in front of declarations
If the current token is not the start of a declaration, check if we can consume unexpected tokens to reach the declaration’s introducer.
1 parent c329343 commit 00cf557

File tree

7 files changed

+221
-21
lines changed

7 files changed

+221
-21
lines changed

Sources/SwiftParser/Declarations.swift

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
@_spi(RawSyntax) import SwiftSyntax
1414

1515
extension TokenConsumer {
16-
func atStartOfDeclaration() -> Bool {
16+
func atStartOfDeclaration(isAtTopLevel: Bool = false, allowRecovery: Bool = false) -> Bool {
1717
if self.at(anyIn: PoundDeclarationStart.self) != nil {
1818
return true
1919
}
@@ -44,8 +44,17 @@ extension TokenConsumer {
4444
}
4545
}
4646

47-
switch subparser.at(anyIn: DeclarationStart.self) {
48-
case (.caseKeyword, _)?, nil:
47+
let declStartKeyword: DeclarationStart?
48+
if allowRecovery {
49+
declStartKeyword = subparser.canRecoverTo(
50+
anyIn: DeclarationStart.self,
51+
recoveryPrecedence: isAtTopLevel ? nil : .strongBracketedClose
52+
)?.0
53+
} else {
54+
declStartKeyword = subparser.at(anyIn: DeclarationStart.self)?.0
55+
}
56+
switch declStartKeyword {
57+
case .caseKeyword, nil:
4958
// When 'case' appears inside a function, it's probably a switch
5059
// case, not an enum case declaration.
5160
return false
@@ -113,7 +122,7 @@ extension Parser {
113122
let attrs = DeclAttributes(
114123
attributes: self.parseAttributeList(),
115124
modifiers: self.parseModifierList())
116-
switch self.at(anyIn: DeclarationStart.self) {
125+
switch self.canRecoverTo(anyIn: DeclarationStart.self) {
117126
case (.importKeyword, _)?:
118127
return RawDeclSyntax(self.parseImportDeclaration(attrs))
119128
case (.classKeyword, _)?:
@@ -1010,8 +1019,7 @@ extension Parser {
10101019
/// actor-member → declaration | compiler-control-statement
10111020
@_spi(RawSyntax)
10121021
public mutating func parseActorDeclaration(_ attrs: DeclAttributes) -> RawActorDeclSyntax {
1013-
assert(self.atContextualKeyword("actor"))
1014-
let actorKeyword = self.expectIdentifierWithoutRecovery()
1022+
let (unexpectedBeforeActorKeyword, actorKeyword) = self.expectContextualKeyword("actor", precedence: .declKeyword)
10151023
let name = self.expectIdentifierWithoutRecovery()
10161024

10171025
let generics: RawGenericParameterClauseSyntax?
@@ -1041,6 +1049,7 @@ extension Parser {
10411049
return RawActorDeclSyntax(
10421050
attributes: attrs.attributes,
10431051
modifiers: attrs.modifiers,
1052+
unexpectedBeforeActorKeyword,
10441053
actorKeyword: actorKeyword,
10451054
identifier: name,
10461055
genericParameterClause: generics,

Sources/SwiftParser/Parser.swift

Lines changed: 76 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,76 @@ extension Parser {
203203

204204
}
205205

206+
// MARK: Check if we can recover to a token
207+
208+
extension Parser {
209+
/// Checks if it can reach a token of the given `kind` by skipping unexpected
210+
/// tokens that have lower ``TokenPrecedence`` than expected token.
211+
@_spi(RawSyntax)
212+
public func canRecoverTo(_ kind: RawTokenKind) -> Bool {
213+
if self.at(kind) {
214+
return true
215+
}
216+
var lookahead = self.lookahead()
217+
return lookahead.canRecoverTo([kind])
218+
}
219+
220+
/// Checks if it can reach a contextual keyword with the given `name` by
221+
/// skipping unexpected tokens that have lower ``TokenPrecedence`` than
222+
/// `precedence`.
223+
@_spi(RawSyntax)
224+
public func canRecoverToContextualKeyword(
225+
_ name: SyntaxText,
226+
precedence: TokenPrecedence = TokenPrecedence(.contextualKeyword)
227+
) -> Bool {
228+
if self.atContextualKeyword(name) {
229+
return true
230+
}
231+
var lookahead = self.lookahead()
232+
return lookahead.canRecoverTo([], contextualKeywords: [name])
233+
}
234+
235+
236+
/// Checks if it can reach a token whose kind is in `kinds` by skipping
237+
/// unexpected tokens that have lower ``TokenPrecedence`` than `precedence`.
238+
@_spi(RawSyntax)
239+
public func canRecoverTo(
240+
any kinds: [RawTokenKind]
241+
) -> Bool {
242+
if self.at(any: kinds) {
243+
return true
244+
}
245+
var lookahead = self.lookahead()
246+
return lookahead.canRecoverTo(kinds)
247+
}
248+
249+
250+
/// Checks if we can reach a token in `subset` by skipping tokens that have
251+
/// a precedence that have a lower ``TokenPrecedence`` than the minimum
252+
/// precedence of a token in that subset.
253+
/// If so, return the token that we can recover to and a handle that can be
254+
/// used to consume the unexpected tokens and the token we recovered to.
255+
func canRecoverTo<Subset: RawTokenKindSubset>(
256+
anyIn subset: Subset.Type
257+
) -> (Subset, RecoveryConsumptionHandle)? {
258+
if let (kind, handle) = self.at(anyIn: subset) {
259+
return (kind, RecoveryConsumptionHandle(unexpectedTokens: 0, tokenConsumptionHandle: handle))
260+
}
261+
var lookahead = self.lookahead()
262+
return lookahead.canRecoverTo(anyIn: subset)
263+
}
264+
265+
/// Eat a token that we know we are currently positioned at, based on `canRecoverTo(anyIn:)`.
266+
mutating func eat(_ handle: RecoveryConsumptionHandle) -> (RawUnexpectedNodesSyntax, Token) {
267+
var unexpectedTokens = [RawSyntax]()
268+
for _ in 0..<handle.unexpectedTokens {
269+
unexpectedTokens.append(RawSyntax(self.consumeAnyToken()))
270+
}
271+
let token = self.eat(handle.tokenConsumptionHandle)
272+
return (RawUnexpectedNodesSyntax(elements: unexpectedTokens, arena: self.arena), token)
273+
}
274+
}
275+
206276
// MARK: Expecting Tokens with Recovery
207277

208278
extension Parser {
@@ -229,7 +299,7 @@ extension Parser {
229299

230300
/// Attempts to consume a token of the given kind.
231301
/// If it cannot be found, the parser tries
232-
/// 1. To each unexpected tokens that have lower ``TokenPrecedence`` than the
302+
/// 1. To eat unexpected tokens that have lower ``TokenPrecedence`` than the
233303
/// expected token and see if the token occurs after that unexpected.
234304
/// 2. If the token couldn't be found after skipping unexpected, it synthesizes
235305
/// a missing token of the requested kind.
@@ -246,24 +316,24 @@ extension Parser {
246316

247317
/// Attempts to consume a contextual keyword whose text is `name`.
248318
/// If it cannot be found, the parser tries
249-
/// 1. To each unexpected tokens that have lower ``TokenPrecedence`` than the
250-
/// expected token and see if the token occurs after that unexpected.
319+
/// 1. To eat unexpected tokens that have lower ``TokenPrecedence`` than `precedence`.
251320
/// 2. If the token couldn't be found after skipping unexpected, it synthesizes
252321
/// a missing token of the requested kind.
253322
@_spi(RawSyntax)
254323
public mutating func expectContextualKeyword(
255-
_ name: SyntaxText
324+
_ name: SyntaxText,
325+
precedence: TokenPrecedence = TokenPrecedence(.contextualKeyword)
256326
) -> (unexpected: RawUnexpectedNodesSyntax?, token: RawTokenSyntax) {
257327
return expectImpl(
258328
consume: { $0.consumeIfContextualKeyword(name) },
259-
canRecoverTo: { $0.canRecoverTo([], contextualKeywords: [name]) },
329+
canRecoverTo: { $0.canRecoverTo([], contextualKeywords: [name], recoveryPrecedence: precedence) },
260330
makeMissing: { $0.missingToken(.contextualKeyword, text: name) }
261331
)
262332
}
263333

264334
/// Attempts to consume a token whose kind is in `kinds`.
265335
/// If it cannot be found, the parser tries
266-
/// 1. To each unexpected tokens that have lower ``TokenPrecedence`` than the
336+
/// 1. To eat unexpected tokens that have lower ``TokenPrecedence`` than the
267337
/// lowest precedence of the expected token kinds and see if a token of
268338
/// the requested kinds occurs after the unexpected.
269339
/// 2. If the token couldn't be found after skipping unexpected, it synthesizes

Sources/SwiftParser/RawTokenKindSubset.swift

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ protocol RawTokenKindSubset: CaseIterable {
2424
/// If not `nil`, the token's will be remapped to this kind when the handle is eaten.
2525
var remappedKind: RawTokenKind? { get }
2626

27+
/// The precedence of this token that determines which tokens can be skipped
28+
/// trying to reach it. If this returns `nil`, the precedence of `rawTokenKind`
29+
/// is used. This is mostly overwritten for contextual keywords.
30+
var precedence: TokenPrecedence? { get }
31+
2732
/// Allows more flexible rejection of further token kinds based on the token's
2833
/// contents. Useful to e.g. look for contextual keywords.
2934
func accepts(lexeme: Lexer.Lexeme) -> Bool
@@ -42,6 +47,10 @@ extension RawTokenKindSubset {
4247
}
4348
}
4449

50+
var precedence: TokenPrecedence? {
51+
return nil
52+
}
53+
4554
func accepts(lexeme: Lexer.Lexeme) -> Bool {
4655
return true
4756
}
@@ -392,6 +401,14 @@ enum DeclarationStart: RawTokenKindSubset {
392401
default: return nil
393402
}
394403
}
404+
405+
var precedence: TokenPrecedence? {
406+
switch self {
407+
case .actorContextualKeyword: return .declKeyword
408+
case .caseKeyword: return .declKeyword
409+
default: return nil
410+
}
411+
}
395412
}
396413

397414
enum EffectsSpecifier: RawTokenKindSubset {

Sources/SwiftParser/Recovery.swift

Lines changed: 61 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,15 @@
1414

1515
// MARK: Lookahead
1616

17+
18+
/// After calling `consume(ifAnyFrom:)` we know which token we are positioned
19+
/// at based on that function's return type. This handle allows consuming that
20+
/// token.
21+
struct RecoveryConsumptionHandle {
22+
var unexpectedTokens: Int
23+
var tokenConsumptionHandle: TokenConsumptionHandle
24+
}
25+
1726
extension Parser.Lookahead {
1827
/// Tries eating tokens until it finds a token whose kind is in `kinds` or a
1928
/// contextual keyword with a text in `contextualKeywords` without skipping
@@ -25,13 +34,15 @@ extension Parser.Lookahead {
2534
/// `lookahead.tokensConsumed` as unexpected.
2635
mutating func canRecoverTo(
2736
_ kinds: [RawTokenKind],
28-
contextualKeywords: [SyntaxText] = []
37+
contextualKeywords: [SyntaxText] = [],
38+
recoveryPrecedence: TokenPrecedence? = nil
2939
) -> Bool {
30-
assert(!kinds.isEmpty)
31-
var recoveryPrecedence = kinds.map(TokenPrecedence.init).min()!
40+
var precedences = kinds.map(TokenPrecedence.init)
3241
if !contextualKeywords.isEmpty {
33-
recoveryPrecedence = min(recoveryPrecedence, TokenPrecedence(.identifier), TokenPrecedence(.contextualKeyword))
42+
precedences += [TokenPrecedence(.identifier), TokenPrecedence(.contextualKeyword)]
3443
}
44+
let recoveryPrecedence = recoveryPrecedence ?? precedences.min()!
45+
3546
while !self.at(.eof) {
3647
if !recoveryPrecedence.shouldSkipOverNewlines,
3748
self.currentToken.isAtStartOfLine {
@@ -55,5 +66,51 @@ extension Parser.Lookahead {
5566

5667
return false
5768
}
69+
70+
/// Checks if we can reach a token in `subset` by skipping tokens that have
71+
/// a precedence that have a lower ``TokenPrecedence`` than the minimum
72+
/// precedence of a token in that subset.
73+
/// If so, return the token that we can recover to and a handle that can be
74+
/// used to consume the unexpected tokens and the token we recovered to.
75+
mutating func canRecoverTo<Subset: RawTokenKindSubset>(
76+
anyIn subset: Subset.Type,
77+
recoveryPrecedence: TokenPrecedence? = nil
78+
) -> (Subset, RecoveryConsumptionHandle)? {
79+
assert(!subset.allCases.isEmpty, "Subset must have at least one case")
80+
let recoveryPrecedence = recoveryPrecedence ?? subset.allCases.map({
81+
if let precedence = $0.precedence {
82+
return precedence
83+
} else {
84+
return TokenPrecedence($0.rawTokenKind)
85+
}
86+
}).min()!
87+
let initialTokensConsumed = self.tokensConsumed
88+
assert(!subset.allCases.isEmpty)
89+
while !self.at(.eof) {
90+
if !recoveryPrecedence.shouldSkipOverNewlines,
91+
self.currentToken.isAtStartOfLine {
92+
break
93+
}
94+
if let (kind, handle) = self.at(anyIn: subset) {
95+
return (kind, RecoveryConsumptionHandle(
96+
unexpectedTokens: self.tokensConsumed - initialTokensConsumed,
97+
tokenConsumptionHandle: handle
98+
))
99+
}
100+
let currentTokenPrecedence = TokenPrecedence(self.currentToken.tokenKind)
101+
if currentTokenPrecedence >= recoveryPrecedence {
102+
break
103+
}
104+
self.consumeAnyToken()
105+
if let closingDelimiter = currentTokenPrecedence.closingTokenKind {
106+
guard self.canRecoverTo([closingDelimiter]) else {
107+
break
108+
}
109+
self.eat(closingDelimiter)
110+
}
111+
}
112+
113+
return nil
114+
}
58115
}
59116

Sources/SwiftParser/TokenPrecedence.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,8 @@ public enum TokenPrecedence: Comparable {
8787
return self >= .stmtKeyword
8888
}
8989

90-
init(_ tokenKind: RawTokenKind) {
90+
@_spi(RawSyntax)
91+
public init(_ tokenKind: RawTokenKind) {
9192
switch tokenKind {
9293
// MARK: Identifier like
9394
case

Sources/SwiftParser/TopLevel.swift

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ extension Parser {
4646
mutating func parseTopLevelCodeBlockItems() -> RawCodeBlockItemListSyntax {
4747
var elements = [RawCodeBlockItemSyntax]()
4848
var loopProgress = LoopProgressCondition()
49-
while let newElement = self.parseCodeBlockItem(), loopProgress.evaluate(currentToken) {
49+
while let newElement = self.parseCodeBlockItem(isAtTopLevel: true), loopProgress.evaluate(currentToken) {
5050
elements.append(newElement)
5151
}
5252
return .init(elements: elements, arena: self.arena)
@@ -113,10 +113,10 @@ extension Parser {
113113
/// statement → compiler-control-statement
114114
/// statements → statement statements?
115115
@_spi(RawSyntax)
116-
public mutating func parseCodeBlockItem() -> RawCodeBlockItemSyntax? {
116+
public mutating func parseCodeBlockItem(isAtTopLevel: Bool = false) -> RawCodeBlockItemSyntax? {
117117
// FIXME: It is unfortunate that the Swift book refers to these as
118118
// "statements" and not "items".
119-
let item = self.parseItem()
119+
let item = self.parseItem(isAtTopLevel: isAtTopLevel)
120120
let semi = self.consume(if: .semicolon)
121121

122122
if item.raw.byteLength == 0 && semi == nil {
@@ -125,7 +125,12 @@ extension Parser {
125125
return .init(item: item, semicolon: semi, errorTokens: nil, arena: self.arena)
126126
}
127127

128-
private mutating func parseItem() -> RawSyntax {
128+
/// `isAtTopLevel` determins whether this is trying to parse an item that's at
129+
/// the top level of the source file. If this is the case, we allow skipping
130+
/// closing braces while trying to recover to the next item.
131+
/// If we are not at the top level, such a closing brace should close the
132+
/// wrapping declaration instead of being consumed by lookeahead.
133+
private mutating func parseItem(isAtTopLevel: Bool = false) -> RawSyntax {
129134
if self.at(.poundIfKeyword) {
130135
return RawSyntax(self.parsePoundIfDirective {
131136
$0.parseCodeBlockItem()
@@ -142,6 +147,8 @@ extension Parser {
142147
return RawSyntax(self.parseStatement())
143148
} else if self.atStartOfExpression() {
144149
return RawSyntax(self.parseExpression())
150+
} else if self.atStartOfDeclaration(isAtTopLevel: isAtTopLevel, allowRecovery: true) {
151+
return RawSyntax(self.parseDeclaration())
145152
} else {
146153
return RawSyntax(RawMissingExprSyntax(arena: self.arena))
147154
}

0 commit comments

Comments
 (0)