Skip to content

Commit 395908e

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 684b2e7 commit 395908e

File tree

10 files changed

+977
-28
lines changed

10 files changed

+977
-28
lines changed

CodeGeneration/Sources/SyntaxSupport/KeywordSpec.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,7 @@ public let KEYWORDS: [KeywordSpec] = [
220220
KeywordSpec("swift"),
221221
KeywordSpec("switch", isLexerClassified: true, requiresTrailingSpace: true),
222222
KeywordSpec("target"),
223+
KeywordSpec("then"),
223224
KeywordSpec("throw", isLexerClassified: true, requiresTrailingSpace: true),
224225
KeywordSpec("throws", isLexerClassified: true, requiresTrailingSpace: true),
225226
KeywordSpec("transpose"),

CodeGeneration/Sources/SyntaxSupport/StmtNodes.swift

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

618+
Node(
619+
kind: .thenStmt,
620+
base: .stmt,
621+
nameForDiagnostics: "'then' statement",
622+
children: [
623+
Child(
624+
name: "ThenKeyword",
625+
kind: .token(choices: [.keyword(text: "then")])
626+
),
627+
Child(
628+
name: "Expression",
629+
kind: .node(kind: .expr)
630+
),
631+
]
632+
),
618633
]

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
@@ -18,3 +18,8 @@ extension Parser {
1818
}
1919
}
2020
}
21+
22+
extension Parser.ExperimentalFeatures {
23+
/// Whether to enable the parsing of 'then' statements.
24+
public static let thenStatements = Self(rawValue: 1 << 0)
25+
}

Sources/SwiftParser/Statements.swift

Lines changed: 106 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,21 @@ 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 an expression or statement could be parsed, returns
22+
/// `false` if this parameter is set to `true`.
23+
///
1924
/// - Note: This function must be kept in sync with `parseStatement()`.
2025
/// - Seealso: ``Parser/parseStatement()``
21-
func atStartOfStatement(allowRecovery: Bool = false) -> Bool {
26+
func atStartOfStatement(allowRecovery: Bool = false, preferExpr: Bool = false) -> Bool {
2227
var lookahead = self.lookahead()
2328
if allowRecovery {
2429
// Attributes are not allowed on statements. But for recovery, skip over
2530
// misplaced attributes.
2631
_ = lookahead.consumeAttributeList()
2732
}
28-
return lookahead.atStartOfStatement(allowRecovery: allowRecovery)
33+
return lookahead.atStartOfStatement(allowRecovery: allowRecovery, preferExpr: preferExpr)
2934
}
3035
}
3136

@@ -105,6 +110,8 @@ extension Parser {
105110
return label(self.parseDoStatement(doHandle: handle), with: optLabel)
106111
case (.yield, let handle)?:
107112
return label(self.parseYieldStatement(yieldHandle: handle), with: optLabel)
113+
case (.then, let handle)?:
114+
return label(self.parseThenStatement(handle: handle), with: optLabel)
108115
case nil:
109116
let missingStmt = RawStmtSyntax(RawMissingStmtSyntax(arena: self.arena))
110117
return label(missingStmt, with: optLabel)
@@ -630,7 +637,7 @@ extension Parser {
630637
if self.at(anyIn: IfOrSwitch.self) != nil {
631638
return true
632639
}
633-
if self.atStartOfStatement() || self.atStartOfDeclaration() {
640+
if self.atStartOfStatement(preferExpr: true) || self.atStartOfDeclaration() {
634641
return false
635642
}
636643
return true
@@ -723,6 +730,35 @@ extension Parser {
723730
}
724731
}
725732

733+
extension Parser {
734+
/// Parse a `then` statement.
735+
mutating func parseThenStatement(handle: RecoveryConsumptionHandle) -> RawStmtSyntax {
736+
guard experimentalFeatures.contains(.thenStatements) else {
737+
return RawStmtSyntax(RawMissingStmtSyntax(arena: self.arena))
738+
}
739+
let (unexpectedBeforeThen, then) = self.eat(handle)
740+
let hasMisplacedTry = unexpectedBeforeThen?.containsToken(where: { TokenSpec(.try) ~= $0 }) ?? false
741+
742+
var expr = self.parseExpression()
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(RawThenStmtSyntax(
754+
unexpectedBeforeThen,
755+
thenKeyword: then,
756+
expression: expr,
757+
arena: self.arena
758+
))
759+
}
760+
}
761+
726762
extension Parser {
727763
struct StatementLabel {
728764
var label: RawTokenSyntax
@@ -791,7 +827,7 @@ extension Parser {
791827
}
792828

793829
guard
794-
self.at(.identifier) && !self.atStartOfStatement() && !self.atStartOfDeclaration()
830+
self.at(.identifier) && !self.atStartOfStatement(preferExpr: true) && !self.atStartOfDeclaration()
795831
else {
796832
return nil
797833
}
@@ -806,9 +842,20 @@ extension Parser.Lookahead {
806842
/// Returns `true` if the current token represents the start of a statement
807843
/// item.
808844
///
845+
/// - Parameters:
846+
/// - allowRecovery: Whether to attempt to perform recovery.
847+
/// - preferExpr: If `true`, if either an expression or statement could be
848+
/// parsed, returns `false` such that an expression can be parsed.
849+
///
809850
/// - Note: This function must be kept in sync with `parseStatement()`.
810851
/// - Seealso: ``Parser/parseStatement()``
852+
<<<<<<< HEAD
811853
mutating func atStartOfStatement(allowRecovery: Bool = false) -> Bool {
854+
||||||| parent of a2c4f722 (Introduce `then` statements)
855+
mutating func isStartOfStatement(allowRecovery: Bool = false) -> Bool {
856+
=======
857+
mutating func isStartOfStatement(allowRecovery: Bool = false, preferExpr: Bool = false) -> Bool {
858+
>>>>>>> a2c4f722 (Introduce `then` statements)
812859
if (self.at(anyIn: SwitchCaseStart.self) != nil || self.at(.atSign)) && withLookahead({ $0.atStartOfSwitchCaseItem() }) {
813860
// We consider SwitchCaseItems statements so we don't parse the start of a new case item as trailing parts of an expression.
814861
return true
@@ -877,11 +924,66 @@ extension Parser.Lookahead {
877924
// For example, could be the function call "discard()".
878925
return false
879926
}
927+
928+
case .then:
929+
return isStartOfThenStatement(preferExpr: preferExpr)
930+
880931
case nil:
881932
return false
882933
}
883934
}
884935

936+
/// Whether we're currently at a `then` token that should be parsed as a
937+
/// `then` statement.
938+
mutating func isStartOfThenStatement(preferExpr: Bool) -> Bool {
939+
// If the feature is disabled, don't parse it.
940+
guard self.experimentalFeatures.contains(.thenStatements) else {
941+
return false
942+
}
943+
guard self.at(.keyword(.then)) else {
944+
return false
945+
}
946+
947+
// If we want to prefer an expr, and aren't at the start of a newline, then
948+
// don't parse a ThenStmt.
949+
if preferExpr && !self.atStartOfLine {
950+
return false
951+
}
952+
953+
let next = peek()
954+
955+
// If 'then' is followed by a binary or postfix operator, prefer to parse as
956+
// an expr.
957+
if BinaryOperatorLike(lexeme: next) != nil || PostfixOperatorLike(lexeme: next) != nil {
958+
return false
959+
}
960+
961+
switch next.rawTokenKind {
962+
case .keyword:
963+
// Treat 'is' and 'as' like the binary operator case, and parse as an
964+
// expr.
965+
switch Keyword(next.tokenText) {
966+
case .as, .is:
967+
return false
968+
default:
969+
break
970+
}
971+
972+
case .leftBrace:
973+
// This is a trailing closure.
974+
return false
975+
976+
case .leftParen, .leftSquare, .period:
977+
// These are handled based on whether there is trivia between the 'then'
978+
// and the token. If so, it's a 'then' statement. Otherwise it should
979+
// be treated as an expression, e.g `then(...)`, `then[...]`, `then.foo`.
980+
return !self.currentToken.trailingTriviaText.isEmpty || !next.leadingTriviaText.isEmpty
981+
default:
982+
break
983+
}
984+
return true
985+
}
986+
885987
/// Returns whether the parser's current position is the start of a switch case,
886988
/// given that we're in the middle of a switch already.
887989
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

Sources/SwiftParser/TokenSpecSet.swift

Lines changed: 66 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ enum CanBeStatementStart: TokenSpecSet {
6666
case `repeat`
6767
case `return`
6868
case `switch`
69+
case then
6970
case `throw`
7071
case `while`
7172
case yield
@@ -85,6 +86,7 @@ enum CanBeStatementStart: TokenSpecSet {
8586
case TokenSpec(.repeat): self = .repeat
8687
case TokenSpec(.return): self = .return
8788
case TokenSpec(.switch): self = .switch
89+
case TokenSpec(.then): self = .then
8890
case TokenSpec(.throw): self = .throw
8991
case TokenSpec(.while): self = .while
9092
case TokenSpec(.yield): self = .yield
@@ -107,6 +109,7 @@ enum CanBeStatementStart: TokenSpecSet {
107109
case .repeat: return .keyword(.repeat)
108110
case .return: return .keyword(.return)
109111
case .switch: return .keyword(.switch)
112+
case .then: return .keyword(.then)
110113
case .throw: return .keyword(.throw)
111114
case .while: return .keyword(.while)
112115
case .yield: return .keyword(.yield)
@@ -478,48 +481,88 @@ enum Operator: TokenSpecSet {
478481
}
479482
}
480483

481-
/// Tokens that can be used in operator declarations
482-
enum OperatorLike: TokenSpecSet {
483-
case `operator`(Operator)
484-
case exclamationMark
484+
/// Tokens that are either binary operators, or can act like binary operators.
485+
enum BinaryOperatorLike: TokenSpecSet {
486+
case binaryOperator
485487
case infixQuestionMark
486-
case postfixQuestionMark
487488
case equal
488489
case arrow
489490

490491
init?(lexeme: Lexer.Lexeme) {
491-
if let op = Operator(lexeme: lexeme) {
492-
self = .operator(op)
493-
return
494-
}
495492
switch lexeme.rawTokenKind {
496-
case .exclamationMark: self = .exclamationMark
493+
case .binaryOperator: self = .binaryOperator
497494
case .infixQuestionMark: self = .infixQuestionMark
498-
case .postfixQuestionMark: self = .postfixQuestionMark
499495
case .equal: self = .equal
500496
case .arrow: self = .arrow
501497
default: return nil
502498
}
503499
}
504500

505-
static var allCases: [OperatorLike] {
506-
return Operator.allCases.map(Self.operator) + [
507-
.exclamationMark,
508-
.infixQuestionMark,
509-
.postfixQuestionMark,
510-
.equal,
511-
.arrow,
512-
]
501+
var spec: TokenSpec {
502+
switch self {
503+
case .binaryOperator: return .binaryOperator
504+
case .infixQuestionMark: return TokenSpec(.infixQuestionMark, remapping: .binaryOperator)
505+
case .equal: return TokenSpec(.equal, remapping: .binaryOperator)
506+
case .arrow: return TokenSpec(.arrow, remapping: .binaryOperator)
507+
}
508+
}
509+
}
510+
511+
/// Tokens that are either postfix operators, or can act like postfix operators.
512+
enum PostfixOperatorLike: TokenSpecSet {
513+
case postfixOperator
514+
case exclamationMark
515+
case postfixQuestionMark
516+
517+
init?(lexeme: Lexer.Lexeme) {
518+
switch lexeme.rawTokenKind {
519+
case .postfixOperator: self = .postfixOperator
520+
case .exclamationMark: self = .exclamationMark
521+
case .postfixQuestionMark: self = .postfixQuestionMark
522+
default: return nil
523+
}
513524
}
514525

515526
var spec: TokenSpec {
516527
switch self {
517-
case .operator(let op): return op.spec
528+
case .postfixOperator: return .postfixOperator
518529
case .exclamationMark: return TokenSpec(.exclamationMark, remapping: .postfixOperator)
519-
case .infixQuestionMark: return TokenSpec(.infixQuestionMark, remapping: .binaryOperator)
520530
case .postfixQuestionMark: return TokenSpec(.postfixQuestionMark, remapping: .postfixOperator)
521-
case .equal: return TokenSpec(.equal, remapping: .binaryOperator)
522-
case .arrow: return TokenSpec(.arrow, remapping: .binaryOperator)
531+
}
532+
}
533+
}
534+
535+
/// Tokens that can be used in operator declarations
536+
enum OperatorLike: TokenSpecSet {
537+
case `operator`(Operator)
538+
case binaryOperatorLike(BinaryOperatorLike)
539+
case postfixOperatorLike(PostfixOperatorLike)
540+
541+
init?(lexeme: Lexer.Lexeme) {
542+
if let op = Operator(lexeme: lexeme) {
543+
self = .operator(op)
544+
return
545+
}
546+
if let binOp = BinaryOperatorLike(lexeme: lexeme) {
547+
self = .binaryOperatorLike(binOp)
548+
return
549+
}
550+
if let postfixOp = PostfixOperatorLike(lexeme: lexeme) {
551+
self = .postfixOperatorLike(postfixOp)
552+
return
553+
}
554+
return nil
555+
}
556+
557+
static var allCases: [OperatorLike] {
558+
Operator.allCases.map(Self.operator) + BinaryOperatorLike.allCases.map(Self.binaryOperatorLike) + PostfixOperatorLike.allCases.map(Self.postfixOperatorLike)
559+
}
560+
561+
var spec: TokenSpec {
562+
switch self {
563+
case .operator(let op): return op.spec
564+
case .binaryOperatorLike(let op): return op.spec
565+
case .postfixOperatorLike(let op): return op.spec
523566
}
524567
}
525568
}

Sources/SwiftParserDiagnostics/ParseDiagnosticsGenerator.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1492,6 +1492,20 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
14921492
return .visitChildren
14931493
}
14941494

1495+
public override func visit(_ node: ThenStmtSyntax) -> SyntaxVisitorContinueKind {
1496+
if shouldSkip(node) {
1497+
return .skipChildren
1498+
}
1499+
exchangeTokens(
1500+
unexpected: node.unexpectedBeforeThenKeyword,
1501+
unexpectedTokenCondition: { $0.tokenKind == .keyword(.try) },
1502+
correctTokens: [node.expression.as(TryExprSyntax.self)?.tryKeyword],
1503+
message: { _ in .tryMustBePlacedOnThenExpr },
1504+
moveFixIt: { MoveTokensAfterFixIt(movedTokens: $0, after: .keyword(.then)) }
1505+
)
1506+
return .visitChildren
1507+
}
1508+
14951509
public override func visit(_ node: SameTypeRequirementSyntax) -> SyntaxVisitorContinueKind {
14961510
if shouldSkip(node) {
14971511
return .skipChildren

Sources/SwiftParserDiagnostics/ParserDiagnosticMessages.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,9 @@ extension DiagnosticMessage where Self == StaticParserError {
243243
public static var tryMustBePlacedOnThrownExpr: Self {
244244
.init("'try' must be placed on the thrown expression")
245245
}
246+
public static var tryMustBePlacedOnThenExpr: Self {
247+
.init("'try' must be placed on the produced expression")
248+
}
246249
public static var tryOnInitialValueExpression: Self {
247250
.init("'try' must be placed on the initial value expression")
248251
}

0 commit comments

Comments
 (0)