Skip to content

Commit 13f2769

Browse files
committed
Introduce then statements
These allow multi-statement `if`/`switch` expression branches that can produce a value at the end by saying `then <expr>`. This is gated behind an experimental feature option pending evolution discussion.
1 parent 9357de5 commit 13f2769

File tree

12 files changed

+1026
-34
lines changed

12 files changed

+1026
-34
lines changed

CodeGeneration/Sources/SyntaxSupport/KeywordSpec.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,7 @@ public let KEYWORDS: [KeywordSpec] = [
235235
KeywordSpec("swift"),
236236
KeywordSpec("switch", isLexerClassified: true, requiresTrailingSpace: true),
237237
KeywordSpec("target"),
238+
KeywordSpec("then", isExperimental: true),
238239
KeywordSpec("throw", isLexerClassified: true, requiresTrailingSpace: true),
239240
KeywordSpec("throws", isLexerClassified: true, requiresTrailingSpace: true),
240241
KeywordSpec("transpose"),

CodeGeneration/Sources/SyntaxSupport/StmtNodes.swift

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -615,4 +615,29 @@ public let STMT_NODES: [Node] = [
615615
]
616616
),
617617

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

CodeGeneration/Sources/SyntaxSupport/SyntaxNodeKind.swift

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

Sources/SwiftParser/ExperimentalFeatures.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,8 @@ extension Parser {
1919
}
2020
}
2121
}
22+
23+
extension Parser.ExperimentalFeatures {
24+
/// Whether to enable the parsing of 'then' statements.
25+
public static let thenStatements = Self(rawValue: 1 << 0)
26+
}

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 current 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
@@ -1552,7 +1554,9 @@ extension Parser {
15521554
// If the next token is at the beginning of a new line and can never start
15531555
// an element, break.
15541556
if self.atStartOfLine
1555-
&& (self.at(.rightBrace, .poundEndif) || self.atStartOfDeclaration() || self.atStartOfStatement())
1557+
&& (self.at(.rightBrace, .poundEndif)
1558+
|| self.atStartOfDeclaration()
1559+
|| self.atStartOfStatement(preferExpr: false))
15561560
{
15571561
break
15581562
}
@@ -2168,7 +2172,10 @@ extension Parser {
21682172
)
21692173
)
21702174
)
2171-
} else if allowStandaloneStmtRecovery && (self.atStartOfExpression() || self.atStartOfStatement() || self.atStartOfDeclaration()) {
2175+
} else if allowStandaloneStmtRecovery
2176+
&& (self.atStartOfExpression() || self.atStartOfStatement(preferExpr: false)
2177+
|| self.atStartOfDeclaration())
2178+
{
21722179
// Synthesize a label for the statement or declaration that isn't covered by a case right now.
21732180
let statements = parseSwitchCaseBody()
21742181
if statements.isEmpty {

Sources/SwiftParser/Statements.swift

Lines changed: 100 additions & 5 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,6 +111,8 @@ 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)
114+
case (.then, let handle)?:
115+
return label(self.parseThenStatement(handle: handle), with: optLabel)
108116
case nil:
109117
let missingStmt = RawStmtSyntax(RawMissingStmtSyntax(arena: self.arena))
110118
return label(missingStmt, with: optLabel)
@@ -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,37 @@ extension Parser {
723731
}
724732
}
725733

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

793832
guard
794-
self.at(.identifier) && !self.atStartOfStatement() && !self.atStartOfDeclaration()
833+
self.at(.identifier) && !self.atStartOfStatement(preferExpr: true) && !self.atStartOfDeclaration()
795834
else {
796835
return nil
797836
}
@@ -806,9 +845,15 @@ extension Parser.Lookahead {
806845
/// Returns `true` if the current token represents the start of a statement
807846
/// item.
808847
///
848+
/// - Parameters:
849+
/// - allowRecovery: Whether to attempt to perform recovery.
850+
/// - preferExpr: If either an expression or statement could be
851+
/// parsed and this parameter is `true`, the function returns `false`
852+
/// such that an expression can be parsed.
853+
///
809854
/// - Note: This function must be kept in sync with `parseStatement()`.
810855
/// - Seealso: ``Parser/parseStatement()``
811-
mutating func atStartOfStatement(allowRecovery: Bool = false) -> Bool {
856+
mutating func atStartOfStatement(allowRecovery: Bool = false, preferExpr: Bool) -> Bool {
812857
if (self.at(anyIn: SwitchCaseStart.self) != nil || self.at(.atSign)) && withLookahead({ $0.atStartOfSwitchCaseItem() }) {
813858
// We consider SwitchCaseItems statements so we don't parse the start of a new case item as trailing parts of an expression.
814859
return true
@@ -877,11 +922,61 @@ extension Parser.Lookahead {
877922
// For example, could be the function call "discard()".
878923
return false
879924
}
925+
926+
case .then:
927+
return atStartOfThenStatement(preferExpr: preferExpr)
928+
880929
case nil:
881930
return false
882931
}
883932
}
884933

934+
/// Whether we're currently at a `then` token that should be parsed as a
935+
/// `then` statement.
936+
mutating func atStartOfThenStatement(preferExpr: Bool) -> Bool {
937+
// If the feature is disabled, don't parse it.
938+
guard self.experimentalFeatures.contains(.thenStatements) else {
939+
return false
940+
}
941+
guard self.at(.keyword(.then)) else {
942+
return false
943+
}
944+
945+
// If we prefer an expr and aren't at the start of a newline, then don't
946+
// parse a ThenStmt.
947+
if preferExpr && !self.atStartOfLine {
948+
return false
949+
}
950+
951+
let next = peek()
952+
953+
// If 'then' is followed by a binary or postfix operator, prefer to parse as
954+
// an expr.
955+
if BinaryOperatorLike(lexeme: next) != nil || PostfixOperatorLike(lexeme: next) != nil {
956+
return false
957+
}
958+
959+
switch PrepareForKeywordMatch(next) {
960+
case TokenSpec(.is), TokenSpec(.as):
961+
// Treat 'is' and 'as' like the binary operator case, and parse as an
962+
// expr.
963+
return false
964+
965+
case .leftBrace:
966+
// This is a trailing closure.
967+
return false
968+
969+
case .leftParen, .leftSquare, .period:
970+
// These are handled based on whether there is trivia between the 'then'
971+
// and the token. If so, it's a 'then' statement. Otherwise it should
972+
// be treated as an expression, e.g `then(...)`, `then[...]`, `then.foo`.
973+
return !self.currentToken.trailingTriviaText.isEmpty || !next.leadingTriviaText.isEmpty
974+
default:
975+
break
976+
}
977+
return true
978+
}
979+
885980
/// Returns whether the parser's current position is the start of a switch case,
886981
/// given that we're in the middle of a switch already.
887982
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
@@ -212,7 +212,7 @@ enum TokenPrecedence: Comparable {
212212
// Secondary parts of control-flow constructs
213213
.case, .catch, .default, .else,
214214
// Return-like statements
215-
.break, .continue, .fallthrough, .return, .throw, .yield:
215+
.break, .continue, .fallthrough, .return, .throw, .then, .yield:
216216
self = .stmtKeyword
217217
// MARK: Decl keywords
218218
case // Types

0 commit comments

Comments
 (0)