Skip to content

Commit 38919b2

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 fedd548 commit 38919b2

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) {
@@ -484,6 +501,21 @@ extension Parser {
484501
)
485502
}
486503

504+
// Try parse an 'if' or 'switch' as an expression. Note we do this here in
505+
// parseUnaryExpression as we don't allow postfix syntax to hang off such
506+
// expressions to avoid ambiguities such as postfix '.member', which can
507+
// currently be parsed as a static dot member for a result builder.
508+
if self.at(.keyword(.switch)) {
509+
return RawExprSyntax(
510+
parseSwitchExpression(switchHandle: .constant(.keyword(.switch)))
511+
)
512+
}
513+
if self.at(.keyword(.if)) {
514+
return RawExprSyntax(
515+
parseIfExpression(ifHandle: .constant(.keyword(.if)))
516+
)
517+
}
518+
487519
switch self.at(anyIn: ExpressionPrefixOperator.self) {
488520
case (.prefixAmpersand, let handle)?:
489521
let amp = self.eat(handle)
@@ -2038,6 +2070,11 @@ extension Parser.Lookahead {
20382070
return false
20392071
}
20402072

2073+
// If this is the start of a switch body, this isn't a trailing closure.
2074+
if self.peek().rawTokenKind == .keyword(.case) {
2075+
return false;
2076+
}
2077+
20412078
// If this is a normal expression (not an expr-basic) then trailing closures
20422079
// are allowed, so this is obviously one.
20432080
// 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
@@ -581,6 +581,26 @@ enum AwaitTryMove: RawTokenKindSubset {
581581
}
582582
}
583583

584+
enum IfOrSwitch: RawTokenKindSubset {
585+
case ifKeyword
586+
case switchKeyword
587+
588+
init?(lexeme: Lexer.Lexeme) {
589+
switch lexeme {
590+
case RawTokenKindMatch(.keyword(.if)): self = .ifKeyword
591+
case RawTokenKindMatch(.keyword(.switch)): self = .switchKeyword
592+
default: return nil
593+
}
594+
}
595+
596+
var rawTokenKind: RawTokenKind {
597+
switch self {
598+
case .ifKeyword: return .keyword(.if)
599+
case .switchKeyword: return .keyword(.switch)
600+
}
601+
}
602+
}
603+
584604
enum ExpressionPrefixOperator: RawTokenKindSubset {
585605
case backslash
586606
case prefixAmpersand

Sources/SwiftParser/Statements.swift

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

630630
extension Parser {
631+
private mutating func isStartOfReturnExpr() -> Bool {
632+
if self.at(any: [
633+
.rightBrace, .keyword(.case), .keyword(.default), .semicolon, .eof,
634+
.poundIfKeyword, .poundEndifKeyword, .poundElseKeyword,
635+
.poundElseifKeyword,
636+
]) {
637+
return false
638+
}
639+
// Allowed for if/switch expressions.
640+
if self.at(anyIn: IfOrSwitch.self) != nil {
641+
return true
642+
}
643+
if self.atStartOfStatement() || self.atStartOfDeclaration() {
644+
return false
645+
}
646+
return true
647+
}
648+
631649
/// Parse a return statement
632650
///
633651
/// Grammar
@@ -643,13 +661,7 @@ extension Parser {
643661
// enclosing stmt-brace to get it by eagerly eating it unless the return is
644662
// followed by a '}', '', statement or decl start keyword sequence.
645663
let expr: RawExprSyntax?
646-
if !self.at(any: [
647-
.rightBrace, .keyword(.case), .keyword(.default), .semicolon, .eof,
648-
.poundIfKeyword, .poundEndifKeyword, .poundElseKeyword,
649-
.poundElseifKeyword,
650-
])
651-
&& !self.atStartOfStatement() && !self.atStartOfDeclaration()
652-
{
664+
if isStartOfReturnExpr() {
653665
let parsedExpr = self.parseExpression()
654666
if hasMisplacedTry && !parsedExpr.is(RawTryExprSyntax.self) {
655667
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.
@@ -222,13 +251,13 @@ extension Parser {
222251
} else if self.atStartOfDeclaration(allowInitDecl: allowInitDecl) {
223252
return .decl(self.parseDeclaration())
224253
} else if self.atStartOfStatement() {
225-
return .stmt(self.parseStatement())
254+
return self.parseStatementItem()
226255
} else if self.atStartOfExpression() {
227256
return .expr(self.parseExpression())
228257
} else if self.atStartOfDeclaration(isAtTopLevel: isAtTopLevel, allowInitDecl: allowInitDecl, allowRecovery: true) {
229258
return .decl(self.parseDeclaration())
230259
} else if self.atStartOfStatement(allowRecovery: true) {
231-
return .stmt(self.parseStatement())
260+
return self.parseStatementItem()
232261
} else {
233262
return .expr(RawExprSyntax(RawMissingExprSyntax(arena: self.arena)))
234263
}

Tests/SwiftParserTest/ExpressionTests.swift

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1170,3 +1170,154 @@ final class MemberExprTests: XCTestCase {
11701170
}
11711171
}
11721172
}
1173+
1174+
final class StatementExpressionTests: XCTestCase {
1175+
func testIfExprInCoercion() {
1176+
AssertParse(
1177+
"""
1178+
func foo() {
1179+
if .random() { 0 } else { 1 } as Int
1180+
}
1181+
"""
1182+
)
1183+
}
1184+
func testSwitchExprInCoercion() {
1185+
AssertParse(
1186+
"""
1187+
func foo() {
1188+
switch Bool.random() { case true: 0 case false: 1 } as Int
1189+
}
1190+
"""
1191+
)
1192+
}
1193+
func testIfExprInReturn() {
1194+
AssertParse(
1195+
"""
1196+
func foo() {
1197+
return if .random() { 0 } else { 1 }
1198+
}
1199+
"""
1200+
)
1201+
}
1202+
func testSwitchExprInReturn() {
1203+
AssertParse(
1204+
"""
1205+
func foo() {
1206+
return switch Bool.random() { case true: 0 case false: 1 }
1207+
}
1208+
"""
1209+
)
1210+
}
1211+
func testTryIf1() {
1212+
AssertParse(
1213+
"""
1214+
func foo() -> Int {
1215+
try if .random() { 0 } else { 1 }
1216+
}
1217+
"""
1218+
)
1219+
}
1220+
func testTryIf2() {
1221+
AssertParse(
1222+
"""
1223+
func foo() -> Int {
1224+
return try if .random() { 0 } else { 1 }
1225+
}
1226+
"""
1227+
)
1228+
}
1229+
func testTryIf3() {
1230+
AssertParse(
1231+
"""
1232+
func foo() -> Int {
1233+
let x = try if .random() { 0 } else { 1 }
1234+
return x
1235+
}
1236+
"""
1237+
)
1238+
}
1239+
func testAwaitIf1() {
1240+
AssertParse(
1241+
"""
1242+
func foo() async -> Int {
1243+
await if .random() { 0 } else { 1 }
1244+
}
1245+
"""
1246+
)
1247+
}
1248+
func testAwaitIf2() {
1249+
AssertParse(
1250+
"""
1251+
func foo() async -> Int {
1252+
return await if .random() { 0 } else { 1 }
1253+
}
1254+
"""
1255+
)
1256+
}
1257+
func testAwaitIf3() {
1258+
AssertParse(
1259+
"""
1260+
func foo() async -> Int {
1261+
let x = await if .random() { 0 } else { 1 }
1262+
return x
1263+
}
1264+
"""
1265+
)
1266+
}
1267+
func testTrySwitch1() {
1268+
AssertParse(
1269+
"""
1270+
func foo() -> Int {
1271+
try switch Bool.random() { case true: 0 case false: 1 }
1272+
}
1273+
"""
1274+
)
1275+
}
1276+
func testTrySwitch2() {
1277+
AssertParse(
1278+
"""
1279+
func foo() -> Int {
1280+
return try switch Bool.random() { case true: 0 case false: 1 }
1281+
}
1282+
"""
1283+
)
1284+
}
1285+
func testTrySwitch3() {
1286+
AssertParse(
1287+
"""
1288+
func foo() -> Int {
1289+
let x = try switch Bool.random() { case true: 0 case false: 1 }
1290+
return x
1291+
}
1292+
"""
1293+
)
1294+
}
1295+
func testAwaitSwitch1() {
1296+
AssertParse(
1297+
"""
1298+
func foo() async -> Int {
1299+
await switch Bool.random() { case true: 0 case false: 1 }
1300+
}
1301+
"""
1302+
)
1303+
}
1304+
func testAwaitSwitch2() {
1305+
AssertParse(
1306+
"""
1307+
func foo() async -> Int {
1308+
return await switch Bool.random() { case true: 0 case false: 1 }
1309+
}
1310+
"""
1311+
)
1312+
}
1313+
func testAwaitSwitch3() {
1314+
AssertParse(
1315+
"""
1316+
func foo() async -> Int {
1317+
let x = await switch Bool.random() { case true: 0 case false: 1 }
1318+
return x
1319+
}
1320+
"""
1321+
)
1322+
}
1323+
}

0 commit comments

Comments
 (0)