Skip to content

Commit f6b7d79

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 9edcf21 commit f6b7d79

File tree

5 files changed

+272
-22
lines changed

5 files changed

+272
-22
lines changed

Sources/SwiftParser/Expressions.swift

Lines changed: 51 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':
@@ -191,6 +196,30 @@ extension Parser {
191196
)
192197
}
193198

199+
/// Parse an unresolved 'as' expression.
200+
///
201+
/// type-casting-operator → 'as' type
202+
/// type-casting-operator → 'as' '?' type
203+
/// type-casting-operator → 'as' '!' type
204+
///
205+
mutating func parseUnresolvedAsExpr(
206+
handle: TokenConsumptionHandle
207+
) -> (operator: RawExprSyntax, rhs: RawExprSyntax) {
208+
let asKeyword = self.eat(handle)
209+
let failable = self.consume(ifAny: [.postfixQuestionMark, .exclamationMark])
210+
let op = RawUnresolvedAsExprSyntax(
211+
asTok: asKeyword,
212+
questionOrExclamationMark: failable,
213+
arena: self.arena
214+
)
215+
216+
// Parse the right type expression operand as part of the 'as' production.
217+
let type = self.parseType()
218+
let rhs = RawTypeExprSyntax(type: type, arena: self.arena)
219+
220+
return (RawExprSyntax(op), RawExprSyntax(rhs))
221+
}
222+
194223
/// Parse an expression sequence operators.
195224
///
196225
/// Returns `nil` if the current token is not at an operator.
@@ -323,19 +352,7 @@ extension Parser {
323352
return (RawExprSyntax(op), RawExprSyntax(rhs))
324353

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

340357
case (.async, _)?:
341358
if self.peek().tokenKind == .arrow || self.peek().tokenKind == .throwsKeyword {
@@ -487,6 +504,22 @@ extension Parser {
487504
) -> RawExprSyntax {
488505
// First check to see if we have the start of a regex literal `/.../`.
489506
// tryLexRegexLiteral(/*forUnappliedOperator*/ false)
507+
508+
// Try parse an 'if' or 'switch' as an expression. Note we do this here in
509+
// parseUnaryExpression as we don't allow postfix syntax to hang off such
510+
// expressions to avoid ambiguities such as postfix '.member', which can
511+
// currently be parsed as a static dot member for a result builder.
512+
if self.at(.keyword(.switch)) {
513+
return RawExprSyntax(
514+
parseSwitchExpression(switchHandle: .constant(.keyword(.switch)))
515+
)
516+
}
517+
if self.at(.keyword(.if)) {
518+
return RawExprSyntax(
519+
parseIfExpression(ifHandle: .constant(.keyword(.if)))
520+
)
521+
}
522+
490523
switch self.at(anyIn: ExpressionPrefixOperator.self) {
491524
case (.prefixAmpersand, let handle)?:
492525
let amp = self.eat(handle)
@@ -2461,6 +2494,11 @@ extension Parser.Lookahead {
24612494
return false
24622495
}
24632496

2497+
// If this is the start of a switch body, this isn't a trailing closure.
2498+
if self.peek().rawTokenKind == .keyword(.case) {
2499+
return false;
2500+
}
2501+
24642502
// If this is a normal expression (not an expr-basic) then trailing closures
24652503
// are allowed, so this is obviously one.
24662504
// 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
@@ -611,6 +611,26 @@ enum AwaitTryMove: RawTokenKindSubset {
611611
}
612612
}
613613

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

Sources/SwiftParser/Statements.swift

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

667667
extension Parser {
668+
private mutating func isStartOfReturnExpr() -> Bool {
669+
if self.at(any: [
670+
.rightBrace, .caseKeyword, .defaultKeyword, .semicolon, .eof,
671+
.poundIfKeyword, .poundErrorKeyword, .poundWarningKeyword,
672+
.poundEndifKeyword, .poundElseKeyword, .poundElseifKeyword,
673+
]) {
674+
return false
675+
}
676+
// Allowed for if/switch expressions.
677+
if self.at(anyIn: IfOrSwitch.self) != nil {
678+
return true
679+
}
680+
if self.atStartOfStatement() || self.atStartOfDeclaration() {
681+
return false
682+
}
683+
return true
684+
}
685+
668686
/// Parse a return statement
669687
///
670688
/// Grammar
@@ -680,13 +698,7 @@ extension Parser {
680698
// enclosing stmt-brace to get it by eagerly eating it unless the return is
681699
// followed by a '}', '', statement or decl start keyword sequence.
682700
let expr: RawExprSyntax?
683-
if !self.at(any: [
684-
.rightBrace, .caseKeyword, .defaultKeyword, .semicolon, .eof,
685-
.poundIfKeyword, .poundErrorKeyword, .poundWarningKeyword,
686-
.poundEndifKeyword, .poundElseKeyword, .poundElseifKeyword,
687-
])
688-
&& !self.atStartOfStatement() && !self.atStartOfDeclaration()
689-
{
701+
if isStartOfReturnExpr() {
690702
let parsedExpr = self.parseExpression()
691703
if hasMisplacedTry && !parsedExpr.is(RawTryExprSyntax.self) {
692704
expr = RawExprSyntax(

Sources/SwiftParser/TopLevel.swift

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,35 @@ extension Parser {
194194
)
195195
}
196196

197+
private mutating func parseStatementItem() -> RawCodeBlockItemSyntax.Item {
198+
let stmt = self.parseStatement()
199+
200+
// Special case: An 'if' or 'switch' statement followed by an 'as' must
201+
// be an if/switch expression in a coercion.
202+
// We could also achieve this by more eagerly attempting to parse an 'if'
203+
// or 'switch' as an expression when in statement position, but that
204+
// could result in less useful recovery behavior.
205+
if at(.keyword(.as)), let stmtExpr = stmt.as(RawExpressionStmtSyntax.self) {
206+
let expr = stmtExpr.expression
207+
if expr.is(RawIfExprSyntax.self) || expr.is(RawSwitchExprSyntax.self) {
208+
let (op, rhs) = parseUnresolvedAsExpr(
209+
handle: .init(tokenKind: .keyword(.as))
210+
)
211+
let sequence = RawExprSyntax(
212+
RawSequenceExprSyntax(
213+
elements: RawExprListSyntax(
214+
elements: [expr, op, rhs],
215+
arena: self.arena
216+
),
217+
arena: self.arena
218+
)
219+
)
220+
return .expr(sequence)
221+
}
222+
}
223+
return .stmt(stmt)
224+
}
225+
197226
/// `isAtTopLevel` determines whether this is trying to parse an item that's at
198227
/// the top level of the source file. If this is the case, we allow skipping
199228
/// closing braces while trying to recover to the next item.
@@ -227,13 +256,13 @@ extension Parser {
227256
} else if self.atStartOfDeclaration(allowInitDecl: allowInitDecl) {
228257
return .decl(self.parseDeclaration())
229258
} else if self.atStartOfStatement() {
230-
return .stmt(self.parseStatement())
259+
return self.parseStatementItem()
231260
} else if self.atStartOfExpression() {
232261
return .expr(self.parseExpression())
233262
} else if self.atStartOfDeclaration(isAtTopLevel: isAtTopLevel, allowInitDecl: allowInitDecl, allowRecovery: true) {
234263
return .decl(self.parseDeclaration())
235264
} else if self.atStartOfStatement(allowRecovery: true) {
236-
return .stmt(self.parseStatement())
265+
return self.parseStatementItem()
237266
} else {
238267
return .expr(RawExprSyntax(RawMissingExprSyntax(arena: self.arena)))
239268
}

Tests/SwiftParserTest/ExpressionTests.swift

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -803,3 +803,154 @@ final class ExpressionTests: XCTestCase {
803803
)
804804
}
805805
}
806+
807+
final class StatementExpressionTests: XCTestCase {
808+
func testIfExprInCoercion() {
809+
AssertParse(
810+
"""
811+
func foo() {
812+
if .random() { 0 } else { 1 } as Int
813+
}
814+
"""
815+
)
816+
}
817+
func testSwitchExprInCoercion() {
818+
AssertParse(
819+
"""
820+
func foo() {
821+
switch Bool.random() { case true: 0 case false: 1 } as Int
822+
}
823+
"""
824+
)
825+
}
826+
func testIfExprInReturn() {
827+
AssertParse(
828+
"""
829+
func foo() {
830+
return if .random() { 0 } else { 1 }
831+
}
832+
"""
833+
)
834+
}
835+
func testSwitchExprInReturn() {
836+
AssertParse(
837+
"""
838+
func foo() {
839+
return switch Bool.random() { case true: 0 case false: 1 }
840+
}
841+
"""
842+
)
843+
}
844+
func testTryIf1() {
845+
AssertParse(
846+
"""
847+
func foo() -> Int {
848+
try if .random() { 0 } else { 1 }
849+
}
850+
"""
851+
)
852+
}
853+
func testTryIf2() {
854+
AssertParse(
855+
"""
856+
func foo() -> Int {
857+
return try if .random() { 0 } else { 1 }
858+
}
859+
"""
860+
)
861+
}
862+
func testTryIf3() {
863+
AssertParse(
864+
"""
865+
func foo() -> Int {
866+
let x = try if .random() { 0 } else { 1 }
867+
return x
868+
}
869+
"""
870+
)
871+
}
872+
func testAwaitIf1() {
873+
AssertParse(
874+
"""
875+
func foo() async -> Int {
876+
await if .random() { 0 } else { 1 }
877+
}
878+
"""
879+
)
880+
}
881+
func testAwaitIf2() {
882+
AssertParse(
883+
"""
884+
func foo() async -> Int {
885+
return await if .random() { 0 } else { 1 }
886+
}
887+
"""
888+
)
889+
}
890+
func testAwaitIf3() {
891+
AssertParse(
892+
"""
893+
func foo() async -> Int {
894+
let x = await if .random() { 0 } else { 1 }
895+
return x
896+
}
897+
"""
898+
)
899+
}
900+
func testTrySwitch1() {
901+
AssertParse(
902+
"""
903+
func foo() -> Int {
904+
try switch Bool.random() { case true: 0 case false: 1 }
905+
}
906+
"""
907+
)
908+
}
909+
func testTrySwitch2() {
910+
AssertParse(
911+
"""
912+
func foo() -> Int {
913+
return try switch Bool.random() { case true: 0 case false: 1 }
914+
}
915+
"""
916+
)
917+
}
918+
func testTrySwitch3() {
919+
AssertParse(
920+
"""
921+
func foo() -> Int {
922+
let x = try switch Bool.random() { case true: 0 case false: 1 }
923+
return x
924+
}
925+
"""
926+
)
927+
}
928+
func testAwaitSwitch1() {
929+
AssertParse(
930+
"""
931+
func foo() async -> Int {
932+
await switch Bool.random() { case true: 0 case false: 1 }
933+
}
934+
"""
935+
)
936+
}
937+
func testAwaitSwitch2() {
938+
AssertParse(
939+
"""
940+
func foo() async -> Int {
941+
return await switch Bool.random() { case true: 0 case false: 1 }
942+
}
943+
"""
944+
)
945+
}
946+
func testAwaitSwitch3() {
947+
AssertParse(
948+
"""
949+
func foo() async -> Int {
950+
let x = await switch Bool.random() { case true: 0 case false: 1 }
951+
return x
952+
}
953+
"""
954+
)
955+
}
956+
}

0 commit comments

Comments
 (0)