Skip to content

Commit 2504b3f

Browse files
authored
Merge pull request #2000 from hamishknight/then
2 parents 29c1564 + 33ed2d6 commit 2504b3f

27 files changed

+1279
-39
lines changed

CodeGeneration/Sources/SyntaxSupport/KeywordSpec.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,7 @@ public enum Keyword: CaseIterable {
255255
case swift
256256
case `switch`
257257
case target
258+
case then
258259
case `throw`
259260
case `throws`
260261
case transpose
@@ -644,6 +645,8 @@ public enum Keyword: CaseIterable {
644645
return KeywordSpec("switch", isLexerClassified: true)
645646
case .target:
646647
return KeywordSpec("target")
648+
case .then:
649+
return KeywordSpec("then")
647650
case .throw:
648651
return KeywordSpec("throw", isLexerClassified: true)
649652
case .throws:

CodeGeneration/Sources/SyntaxSupport/StmtNodes.swift

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -613,4 +613,31 @@ public let STMT_NODES: [Node] = [
613613
]
614614
),
615615

616+
// then-stmt -> 'then' expr ';'?
617+
Node(
618+
kind: .thenStmt,
619+
base: .stmt,
620+
// FIXME: This should be marked experimental.
621+
isExperimental: false,
622+
nameForDiagnostics: "'then' statement",
623+
documentation: """
624+
A statement used to indicate the produced value from an if/switch
625+
expression.
626+
627+
Written as:
628+
```swift
629+
then <expr>
630+
```
631+
""",
632+
children: [
633+
Child(
634+
name: "thenKeyword",
635+
kind: .token(choices: [.keyword(.then)])
636+
),
637+
Child(
638+
name: "expression",
639+
kind: .node(kind: .expr)
640+
),
641+
]
642+
),
616643
]

CodeGeneration/Sources/SyntaxSupport/SyntaxNodeKind.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,7 @@ public enum SyntaxNodeKind: String, CaseIterable {
267267
case switchDefaultLabel
268268
case switchExpr
269269
case ternaryExpr
270+
case thenStmt
270271
case throwStmt
271272
case tryExpr
272273
case tupleExpr

Sources/SwiftParser/ExperimentalFeatures.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,8 @@ extension Parser {
2222
public static let referenceBindings = Self(rawValue: 1 << 0)
2323
}
2424
}
25+
26+
extension Parser.ExperimentalFeatures {
27+
/// Whether to enable the parsing of 'then' statements.
28+
public static let thenStatements = Self(rawValue: 1 << 0)
29+
}

Sources/SwiftParser/Expressions.swift

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@ extension TokenConsumer {
2424
if backtrack.at(anyIn: IfOrSwitch.self) != nil {
2525
return true
2626
}
27-
if backtrack.atStartOfDeclaration() || backtrack.atStartOfStatement() {
27+
// Note we currently pass `preferExpr: false` to prefer diagnosing `try then`
28+
// as needing to be `then try`, rather than parsing `then` as an expression.
29+
if backtrack.atStartOfDeclaration() || backtrack.atStartOfStatement(preferExpr: false) {
2830
// If after the 'try' we are at a declaration or statement, it can't be a valid expression.
2931
// Decide how we want to consume the 'try':
3032
// If the declaration or statement starts at a new line, the user probably just forgot to write the expression after 'try' -> parse it as a TryExpr
@@ -1545,7 +1547,9 @@ extension Parser {
15451547
// If the next token is at the beginning of a new line and can never start
15461548
// an element, break.
15471549
if self.atStartOfLine
1548-
&& (self.at(.rightBrace, .poundEndif) || self.atStartOfDeclaration() || self.atStartOfStatement())
1550+
&& (self.at(.rightBrace, .poundEndif)
1551+
|| self.atStartOfDeclaration()
1552+
|| self.atStartOfStatement(preferExpr: false))
15491553
{
15501554
break
15511555
}
@@ -2166,7 +2170,10 @@ extension Parser {
21662170
)
21672171
)
21682172
)
2169-
} else if allowStandaloneStmtRecovery && (self.atStartOfExpression() || self.atStartOfStatement() || self.atStartOfDeclaration()) {
2173+
} else if allowStandaloneStmtRecovery
2174+
&& (self.atStartOfExpression() || self.atStartOfStatement(preferExpr: false)
2175+
|| self.atStartOfDeclaration())
2176+
{
21702177
// Synthesize a label for the statement or declaration that isn't covered by a case right now.
21712178
let statements = parseSwitchCaseBody()
21722179
if statements.isEmpty {

Sources/SwiftParser/Statements.swift

Lines changed: 97 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,22 @@ extension TokenConsumer {
1616
/// Returns `true` if the current token represents the start of a statement
1717
/// item.
1818
///
19+
/// - Parameters:
20+
/// - allowRecovery: Whether to attempt to perform recovery.
21+
/// - preferExpr: If either an expression or statement could be
22+
/// parsed and this parameter is `true`, the function returns `false`
23+
/// such that an expression can be parsed.
24+
///
1925
/// - Note: This function must be kept in sync with `parseStatement()`.
2026
/// - Seealso: ``Parser/parseStatement()``
21-
func atStartOfStatement(allowRecovery: Bool = false) -> Bool {
27+
func atStartOfStatement(allowRecovery: Bool = false, preferExpr: Bool) -> Bool {
2228
var lookahead = self.lookahead()
2329
if allowRecovery {
2430
// Attributes are not allowed on statements. But for recovery, skip over
2531
// misplaced attributes.
2632
_ = lookahead.consumeAttributeList()
2733
}
28-
return lookahead.atStartOfStatement(allowRecovery: allowRecovery)
34+
return lookahead.atStartOfStatement(allowRecovery: allowRecovery, preferExpr: preferExpr)
2935
}
3036
}
3137

@@ -105,7 +111,9 @@ extension Parser {
105111
return label(self.parseDoStatement(doHandle: handle), with: optLabel)
106112
case (.yield, let handle)?:
107113
return label(self.parseYieldStatement(yieldHandle: handle), with: optLabel)
108-
case nil:
114+
case (.then, let handle)? where experimentalFeatures.contains(.thenStatements):
115+
return label(self.parseThenStatement(handle: handle), with: optLabel)
116+
case nil, (.then, _)?:
109117
let missingStmt = RawStmtSyntax(RawMissingStmtSyntax(arena: self.arena))
110118
return label(missingStmt, with: optLabel)
111119
}
@@ -630,7 +638,7 @@ extension Parser {
630638
if self.at(anyIn: IfOrSwitch.self) != nil {
631639
return true
632640
}
633-
if self.atStartOfStatement() || self.atStartOfDeclaration() {
641+
if self.atStartOfStatement(preferExpr: true) || self.atStartOfDeclaration() {
634642
return false
635643
}
636644
return true
@@ -723,6 +731,36 @@ extension Parser {
723731
}
724732
}
725733

734+
extension Parser {
735+
/// Parse a `then` statement.
736+
mutating func parseThenStatement(handle: RecoveryConsumptionHandle) -> RawStmtSyntax {
737+
assert(experimentalFeatures.contains(.thenStatements))
738+
739+
let (unexpectedBeforeThen, then) = self.eat(handle)
740+
let hasMisplacedTry = unexpectedBeforeThen?.containsToken(where: { TokenSpec(.try) ~= $0 }) ?? false
741+
742+
var expr = self.parseExpression(flavor: .basic, pattern: .none)
743+
if hasMisplacedTry && !expr.is(RawTryExprSyntax.self) {
744+
expr = RawExprSyntax(
745+
RawTryExprSyntax(
746+
tryKeyword: missingToken(.try),
747+
questionOrExclamationMark: nil,
748+
expression: expr,
749+
arena: self.arena
750+
)
751+
)
752+
}
753+
return RawStmtSyntax(
754+
RawThenStmtSyntax(
755+
unexpectedBeforeThen,
756+
thenKeyword: then,
757+
expression: expr,
758+
arena: self.arena
759+
)
760+
)
761+
}
762+
}
763+
726764
extension Parser {
727765
struct StatementLabel {
728766
var label: RawTokenSyntax
@@ -791,7 +829,7 @@ extension Parser {
791829
}
792830

793831
guard
794-
self.at(.identifier) && !self.atStartOfStatement() && !self.atStartOfDeclaration()
832+
self.at(.identifier) && !self.atStartOfStatement(preferExpr: true) && !self.atStartOfDeclaration()
795833
else {
796834
return nil
797835
}
@@ -806,9 +844,15 @@ extension Parser.Lookahead {
806844
/// Returns `true` if the current token represents the start of a statement
807845
/// item.
808846
///
847+
/// - Parameters:
848+
/// - allowRecovery: Whether to attempt to perform recovery.
849+
/// - preferExpr: If either an expression or statement could be
850+
/// parsed and this parameter is `true`, the function returns `false`
851+
/// such that an expression can be parsed.
852+
///
809853
/// - Note: This function must be kept in sync with `parseStatement()`.
810854
/// - Seealso: ``Parser/parseStatement()``
811-
mutating func atStartOfStatement(allowRecovery: Bool = false) -> Bool {
855+
mutating func atStartOfStatement(allowRecovery: Bool = false, preferExpr: Bool) -> Bool {
812856
if (self.at(anyIn: SwitchCaseStart.self) != nil || self.at(.atSign)) && withLookahead({ $0.atStartOfSwitchCaseItem() }) {
813857
// We consider SwitchCaseItems statements so we don't parse the start of a new case item as trailing parts of an expression.
814858
return true
@@ -877,11 +921,57 @@ extension Parser.Lookahead {
877921
// For example, could be the function call "discard()".
878922
return false
879923
}
880-
case nil:
924+
925+
case .then where experimentalFeatures.contains(.thenStatements):
926+
return atStartOfThenStatement(preferExpr: preferExpr)
927+
928+
case nil, .then:
881929
return false
882930
}
883931
}
884932

933+
/// Whether we're currently at a `then` token that should be parsed as a
934+
/// `then` statement.
935+
mutating func atStartOfThenStatement(preferExpr: Bool) -> Bool {
936+
guard self.at(.keyword(.then)) else {
937+
return false
938+
}
939+
940+
// If we prefer an expr and aren't at the start of a newline, then don't
941+
// parse a ThenStmt.
942+
if preferExpr && !self.atStartOfLine {
943+
return false
944+
}
945+
946+
let next = peek()
947+
948+
// If 'then' is followed by a binary or postfix operator, prefer to parse as
949+
// an expr.
950+
if BinaryOperatorLike(lexeme: next) != nil || PostfixOperatorLike(lexeme: next) != nil {
951+
return false
952+
}
953+
954+
switch PrepareForKeywordMatch(next) {
955+
case TokenSpec(.is), TokenSpec(.as):
956+
// Treat 'is' and 'as' like the binary operator case, and parse as an
957+
// expr.
958+
return false
959+
960+
case .leftBrace:
961+
// This is a trailing closure.
962+
return false
963+
964+
case .leftParen, .leftSquare, .period:
965+
// These are handled based on whether there is trivia between the 'then'
966+
// and the token. If so, it's a 'then' statement. Otherwise it should
967+
// be treated as an expression, e.g `then(...)`, `then[...]`, `then.foo`.
968+
return !self.currentToken.trailingTriviaText.isEmpty || !next.leadingTriviaText.isEmpty
969+
default:
970+
break
971+
}
972+
return true
973+
}
974+
885975
/// Returns whether the parser's current position is the start of a switch case,
886976
/// given that we're in the middle of a switch already.
887977
mutating func atStartOfSwitchCase(allowRecovery: Bool = false) -> Bool {

Sources/SwiftParser/TokenPrecedence.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ enum TokenPrecedence: Comparable {
210210
// Secondary parts of control-flow constructs
211211
.case, .catch, .default, .else,
212212
// Return-like statements
213-
.break, .continue, .fallthrough, .return, .throw, .yield:
213+
.break, .continue, .fallthrough, .return, .throw, .then, .yield:
214214
self = .stmtKeyword
215215
// MARK: Decl keywords
216216
case // Types

0 commit comments

Comments
 (0)