Skip to content

Commit 09de565

Browse files
committed
Parse IfExpr and SwitchExpr in expression position
Start parsing if and switch expressions as unary expressions (as we don't allow postfix grammar for them). In addition, parse if/switch expressions in statement position if we see a `try`/`await`/`move`, or a trailing `as Type`.
1 parent 71c34e6 commit 09de565

File tree

5 files changed

+271
-22
lines changed

5 files changed

+271
-22
lines changed

Sources/SwiftParser/Expressions.swift

Lines changed: 50 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ extension TokenConsumer {
1818
case (.awaitTryMove, let handle)?:
1919
var backtrack = self.lookahead()
2020
backtrack.eat(handle)
21+
22+
// These can be parsed as expressions with try/await.
23+
if backtrack.at(anyIn: IfOrSwitch.self) != nil {
24+
return true
25+
}
2126
if backtrack.atStartOfDeclaration() || backtrack.atStartOfStatement() {
2227
// If after the 'try' we are at a declaration or statement, it can't be a valid expression.
2328
// Decide how we want to consume the 'try':
@@ -202,6 +207,30 @@ extension Parser {
202207
)
203208
}
204209

210+
/// Parse an unresolved 'as' expression.
211+
///
212+
/// type-casting-operator → 'as' type
213+
/// type-casting-operator → 'as' '?' type
214+
/// type-casting-operator → 'as' '!' type
215+
///
216+
mutating func parseUnresolvedAsExpr(
217+
handle: TokenConsumptionHandle
218+
) -> (operator: RawExprSyntax, rhs: RawExprSyntax) {
219+
let asKeyword = self.eat(handle)
220+
let failable = self.consume(ifAny: [.postfixQuestionMark, .exclamationMark])
221+
let op = RawUnresolvedAsExprSyntax(
222+
asTok: asKeyword,
223+
questionOrExclamationMark: failable,
224+
arena: self.arena
225+
)
226+
227+
// Parse the right type expression operand as part of the 'as' production.
228+
let type = self.parseType()
229+
let rhs = RawTypeExprSyntax(type: type, arena: self.arena)
230+
231+
return (RawExprSyntax(op), RawExprSyntax(rhs))
232+
}
233+
205234
/// Parse an expression sequence operators.
206235
///
207236
/// Returns `nil` if the current token is not at an operator.
@@ -324,19 +353,7 @@ extension Parser {
324353
return (RawExprSyntax(op), RawExprSyntax(rhs))
325354

326355
case (.asKeyword, let handle)?:
327-
let asKeyword = self.eat(handle)
328-
let failable = self.consume(ifAny: [.postfixQuestionMark, .exclamationMark])
329-
let op = RawUnresolvedAsExprSyntax(
330-
asTok: asKeyword,
331-
questionOrExclamationMark: failable,
332-
arena: self.arena
333-
)
334-
335-
// Parse the right type expression operand as part of the 'as' production.
336-
let type = self.parseType()
337-
let rhs = RawTypeExprSyntax(type: type, arena: self.arena)
338-
339-
return (RawExprSyntax(op), RawExprSyntax(rhs))
356+
return parseUnresolvedAsExpr(handle: handle)
340357

341358
case (.async, _)?:
342359
if self.peek().rawTokenKind == .arrow || self.peek().rawTokenKind == .keyword(.throws) {
@@ -510,6 +527,21 @@ extension Parser {
510527
)
511528
}
512529

530+
// Try parse an 'if' or 'switch' as an expression. Note we do this here in
531+
// parseUnaryExpression as we don't allow postfix syntax to hang off such
532+
// expressions to avoid ambiguities such as postfix '.member', which can
533+
// currently be parsed as a static dot member for a result builder.
534+
if self.at(.keyword(.switch)) {
535+
return RawExprSyntax(
536+
parseSwitchExpression(switchHandle: .constant(.keyword(.switch)))
537+
)
538+
}
539+
if self.at(.keyword(.if)) {
540+
return RawExprSyntax(
541+
parseIfExpression(ifHandle: .constant(.keyword(.if)))
542+
)
543+
}
544+
513545
switch self.at(anyIn: ExpressionPrefixOperator.self) {
514546
case (.prefixAmpersand, let handle)?:
515547
let amp = self.eat(handle)
@@ -2515,6 +2547,11 @@ extension Parser.Lookahead {
25152547
return false
25162548
}
25172549

2550+
// If this is the start of a switch body, this isn't a trailing closure.
2551+
if self.peek().rawTokenKind == .keyword(.case) {
2552+
return false;
2553+
}
2554+
25182555
// If this is a normal expression (not an expr-basic) then trailing closures
25192556
// are allowed, so this is obviously one.
25202557
// TODO: We could handle try to disambiguate cases like:

Sources/SwiftParser/RawTokenKindSubset.swift

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -612,6 +612,26 @@ enum AwaitTryMove: RawTokenKindSubset {
612612
}
613613
}
614614

615+
enum IfOrSwitch: RawTokenKindSubset {
616+
case ifKeyword
617+
case switchKeyword
618+
619+
init?(lexeme: Lexer.Lexeme) {
620+
switch lexeme {
621+
case RawTokenKindMatch(.keyword(.if)): self = .ifKeyword
622+
case RawTokenKindMatch(.keyword(.switch)): self = .switchKeyword
623+
default: return nil
624+
}
625+
}
626+
627+
var rawTokenKind: RawTokenKind {
628+
switch self {
629+
case .ifKeyword: return .keyword(.if)
630+
case .switchKeyword: return .keyword(.switch)
631+
}
632+
}
633+
}
634+
615635
enum ExpressionPrefixOperator: RawTokenKindSubset {
616636
case backslash
617637
case prefixAmpersand

Sources/SwiftParser/Statements.swift

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -654,6 +654,24 @@ extension Parser {
654654
// MARK: Control Transfer Statements
655655

656656
extension Parser {
657+
private func isStartOfReturnExpr() -> Bool {
658+
if self.at(any: [
659+
.rightBrace, .keyword(.case), .keyword(.default), .semicolon, .eof,
660+
.poundIfKeyword, .poundErrorKeyword, .poundWarningKeyword,
661+
.poundEndifKeyword, .poundElseKeyword, .poundElseifKeyword,
662+
]) {
663+
return false
664+
}
665+
// Allowed for if/switch expressions.
666+
if self.at(anyIn: IfOrSwitch.self) != nil {
667+
return true
668+
}
669+
if self.atStartOfStatement() || self.atStartOfDeclaration() {
670+
return false
671+
}
672+
return true
673+
}
674+
657675
/// Parse a return statement
658676
///
659677
/// Grammar
@@ -669,13 +687,7 @@ extension Parser {
669687
// enclosing stmt-brace to get it by eagerly eating it unless the return is
670688
// followed by a '}', '', statement or decl start keyword sequence.
671689
let expr: RawExprSyntax?
672-
if !self.at(any: [
673-
.rightBrace, .keyword(.case), .keyword(.default), .semicolon, .eof,
674-
.poundIfKeyword, .poundErrorKeyword, .poundWarningKeyword,
675-
.poundEndifKeyword, .poundElseKeyword, .poundElseifKeyword,
676-
])
677-
&& !self.atStartOfStatement() && !self.atStartOfDeclaration()
678-
{
690+
if isStartOfReturnExpr() {
679691
let parsedExpr = self.parseExpression()
680692
if hasMisplacedTry && !parsedExpr.is(RawTryExprSyntax.self) {
681693
expr = RawExprSyntax(

Sources/SwiftParser/TopLevel.swift

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,35 @@ extension Parser {
189189
)
190190
}
191191

192+
private mutating func parseStatementItem() -> RawCodeBlockItemSyntax.Item {
193+
let stmt = self.parseStatement()
194+
195+
// Special case: An 'if' or 'switch' statement followed by an 'as' must
196+
// be an if/switch expression in a coercion.
197+
// We could also achieve this by more eagerly attempting to parse an 'if'
198+
// or 'switch' as an expression when in statement position, but that
199+
// could result in less useful recovery behavior.
200+
if at(.keyword(.as)), let stmtExpr = stmt.as(RawExpressionStmtSyntax.self) {
201+
let expr = stmtExpr.expression
202+
if expr.is(RawIfExprSyntax.self) || expr.is(RawSwitchExprSyntax.self) {
203+
let (op, rhs) = parseUnresolvedAsExpr(
204+
handle: .init(tokenKind: .keyword(.as))
205+
)
206+
let sequence = RawExprSyntax(
207+
RawSequenceExprSyntax(
208+
elements: RawExprListSyntax(
209+
elements: [expr, op, rhs],
210+
arena: self.arena
211+
),
212+
arena: self.arena
213+
)
214+
)
215+
return .expr(sequence)
216+
}
217+
}
218+
return .stmt(stmt)
219+
}
220+
192221
/// `isAtTopLevel` determines whether this is trying to parse an item that's at
193222
/// the top level of the source file. If this is the case, we allow skipping
194223
/// closing braces while trying to recover to the next item.
@@ -220,13 +249,13 @@ extension Parser {
220249
} else if self.atStartOfDeclaration(allowInitDecl: allowInitDecl) {
221250
return .decl(self.parseDeclaration())
222251
} else if self.atStartOfStatement() {
223-
return .stmt(self.parseStatement())
252+
return self.parseStatementItem()
224253
} else if self.atStartOfExpression() {
225254
return .expr(self.parseExpression())
226255
} else if self.atStartOfDeclaration(isAtTopLevel: isAtTopLevel, allowInitDecl: allowInitDecl, allowRecovery: true) {
227256
return .decl(self.parseDeclaration())
228257
} else if self.atStartOfStatement(allowRecovery: true) {
229-
return .stmt(self.parseStatement())
258+
return self.parseStatementItem()
230259
} else {
231260
return .expr(RawExprSyntax(RawMissingExprSyntax(arena: self.arena)))
232261
}

Tests/SwiftParserTest/ExpressionTests.swift

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -897,3 +897,154 @@ final class MemberExprTests: XCTestCase {
897897
}
898898
}
899899
}
900+
901+
final class StatementExpressionTests: XCTestCase {
902+
func testIfExprInCoercion() {
903+
AssertParse(
904+
"""
905+
func foo() {
906+
if .random() { 0 } else { 1 } as Int
907+
}
908+
"""
909+
)
910+
}
911+
func testSwitchExprInCoercion() {
912+
AssertParse(
913+
"""
914+
func foo() {
915+
switch Bool.random() { case true: 0 case false: 1 } as Int
916+
}
917+
"""
918+
)
919+
}
920+
func testIfExprInReturn() {
921+
AssertParse(
922+
"""
923+
func foo() {
924+
return if .random() { 0 } else { 1 }
925+
}
926+
"""
927+
)
928+
}
929+
func testSwitchExprInReturn() {
930+
AssertParse(
931+
"""
932+
func foo() {
933+
return switch Bool.random() { case true: 0 case false: 1 }
934+
}
935+
"""
936+
)
937+
}
938+
func testTryIf1() {
939+
AssertParse(
940+
"""
941+
func foo() -> Int {
942+
try if .random() { 0 } else { 1 }
943+
}
944+
"""
945+
)
946+
}
947+
func testTryIf2() {
948+
AssertParse(
949+
"""
950+
func foo() -> Int {
951+
return try if .random() { 0 } else { 1 }
952+
}
953+
"""
954+
)
955+
}
956+
func testTryIf3() {
957+
AssertParse(
958+
"""
959+
func foo() -> Int {
960+
let x = try if .random() { 0 } else { 1 }
961+
return x
962+
}
963+
"""
964+
)
965+
}
966+
func testAwaitIf1() {
967+
AssertParse(
968+
"""
969+
func foo() async -> Int {
970+
await if .random() { 0 } else { 1 }
971+
}
972+
"""
973+
)
974+
}
975+
func testAwaitIf2() {
976+
AssertParse(
977+
"""
978+
func foo() async -> Int {
979+
return await if .random() { 0 } else { 1 }
980+
}
981+
"""
982+
)
983+
}
984+
func testAwaitIf3() {
985+
AssertParse(
986+
"""
987+
func foo() async -> Int {
988+
let x = await if .random() { 0 } else { 1 }
989+
return x
990+
}
991+
"""
992+
)
993+
}
994+
func testTrySwitch1() {
995+
AssertParse(
996+
"""
997+
func foo() -> Int {
998+
try switch Bool.random() { case true: 0 case false: 1 }
999+
}
1000+
"""
1001+
)
1002+
}
1003+
func testTrySwitch2() {
1004+
AssertParse(
1005+
"""
1006+
func foo() -> Int {
1007+
return try switch Bool.random() { case true: 0 case false: 1 }
1008+
}
1009+
"""
1010+
)
1011+
}
1012+
func testTrySwitch3() {
1013+
AssertParse(
1014+
"""
1015+
func foo() -> Int {
1016+
let x = try switch Bool.random() { case true: 0 case false: 1 }
1017+
return x
1018+
}
1019+
"""
1020+
)
1021+
}
1022+
func testAwaitSwitch1() {
1023+
AssertParse(
1024+
"""
1025+
func foo() async -> Int {
1026+
await switch Bool.random() { case true: 0 case false: 1 }
1027+
}
1028+
"""
1029+
)
1030+
}
1031+
func testAwaitSwitch2() {
1032+
AssertParse(
1033+
"""
1034+
func foo() async -> Int {
1035+
return await switch Bool.random() { case true: 0 case false: 1 }
1036+
}
1037+
"""
1038+
)
1039+
}
1040+
func testAwaitSwitch3() {
1041+
AssertParse(
1042+
"""
1043+
func foo() async -> Int {
1044+
let x = await switch Bool.random() { case true: 0 case false: 1 }
1045+
return x
1046+
}
1047+
"""
1048+
)
1049+
}
1050+
}

0 commit comments

Comments
 (0)