Skip to content

Commit 32f793f

Browse files
committed
Parse _borrowing x contextually as a pattern binding when .borrowingSwitch feature is enabled.
The existing parsing under `.referenceBindings` parses this (along with `_mutating` and `_consuming`) as unconditional keywords, which would be a source break we can't ship. Narrow the behavior so that `_borrowing` is only parsed immediately before an identifier in a pattern.
1 parent 2664baa commit 32f793f

File tree

8 files changed

+210
-35
lines changed

8 files changed

+210
-35
lines changed

CodeGeneration/Sources/SyntaxSupport/ExperimentalFeatures.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ public enum ExperimentalFeature: String, CaseIterable {
1818
case doExpressions
1919
case nonescapableTypes
2020
case transferringArgsAndResults
21+
case borrowingSwitch
2122

2223
/// The name of the feature, which is used in the doc comment.
2324
public var featureName: String {
@@ -32,6 +33,8 @@ public enum ExperimentalFeature: String, CaseIterable {
3233
return "NonEscableTypes"
3334
case .transferringArgsAndResults:
3435
return "TransferringArgsAndResults"
36+
case .borrowingSwitch:
37+
return "borrowing pattern matching"
3538
}
3639
}
3740

CodeGeneration/Sources/SyntaxSupport/KeywordSpec.swift

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ public struct KeywordSpec {
1919
/// The experimental feature the keyword is part of, or `nil` if this isn't
2020
/// for an experimental feature.
2121
public let experimentalFeature: ExperimentalFeature?
22+
public let experimentalFeature2: ExperimentalFeature?
2223

2324
/// Indicates if the token kind is switched from being an identifier to a keyword in the lexer.
2425
public let isLexerClassified: Bool
@@ -59,6 +60,26 @@ public struct KeywordSpec {
5960
) {
6061
self.name = name
6162
self.experimentalFeature = experimentalFeature
63+
self.experimentalFeature2 = nil
64+
self.isLexerClassified = isLexerClassified
65+
}
66+
67+
/// Initializes a new `KeywordSpec` instance.
68+
///
69+
/// - Parameters:
70+
/// - name: A name of the keyword.
71+
/// - experimentalFeature: The experimental feature the keyword is part of, or `nil` if this isn't for an experimental feature.
72+
/// - or: A second experimental feature the keyword is also part of, or `nil` if this isn't for an experimental feature.
73+
/// - isLexerClassified: Indicates if the token kind is switched from being an identifier to a keyword in the lexer.
74+
init(
75+
_ name: String,
76+
experimentalFeature: ExperimentalFeature,
77+
or experimentalFeature2: ExperimentalFeature,
78+
isLexerClassified: Bool = false
79+
) {
80+
self.name = name
81+
self.experimentalFeature = experimentalFeature
82+
self.experimentalFeature2 = experimentalFeature2
6283
self.isLexerClassified = isLexerClassified
6384
}
6485
}
@@ -720,7 +741,7 @@ public enum Keyword: CaseIterable {
720741
case .yield:
721742
return KeywordSpec("yield")
722743
case ._borrowing:
723-
return KeywordSpec("_borrowing", experimentalFeature: .referenceBindings)
744+
return KeywordSpec("_borrowing", experimentalFeature: .referenceBindings, or: .borrowingSwitch)
724745
case ._consuming:
725746
return KeywordSpec("_consuming", experimentalFeature: .referenceBindings)
726747
case ._mutating:

CodeGeneration/Sources/generate-swift-syntax/templates/swiftparser/ParserTokenSpecSetFile.swift

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,14 @@ import SwiftSyntaxBuilder
1515
import SyntaxSupport
1616
import Utils
1717

18-
func tokenCaseMatch(_ caseName: TokenSyntax, experimentalFeature: ExperimentalFeature?) -> SwitchCaseSyntax {
19-
let whereClause =
20-
experimentalFeature.map {
21-
"where experimentalFeatures.contains(.\($0.token))"
22-
} ?? ""
18+
func tokenCaseMatch(_ caseName: TokenSyntax, experimentalFeature: ExperimentalFeature?, experimentalFeature2: ExperimentalFeature?) -> SwitchCaseSyntax {
19+
var whereClause = ""
20+
if let feature = experimentalFeature {
21+
whereClause += "where experimentalFeatures.contains(.\(feature.token))"
22+
if let feature2 = experimentalFeature2 {
23+
whereClause += " || experimentalFeatures.contains(.\(feature2.token))"
24+
}
25+
}
2326
return "case TokenSpec(.\(caseName))\(raw: whereClause): self = .\(caseName)"
2427
}
2528

@@ -57,12 +60,14 @@ let parserTokenSpecSetFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
5760
case .keyword(let keyword):
5861
tokenCaseMatch(
5962
keyword.spec.varOrCaseName,
60-
experimentalFeature: keyword.spec.experimentalFeature
63+
experimentalFeature: keyword.spec.experimentalFeature,
64+
experimentalFeature2: keyword.spec.experimentalFeature2
6165
)
6266
case .token(let token):
6367
tokenCaseMatch(
6468
token.spec.varOrCaseName,
65-
experimentalFeature: token.spec.experimentalFeature
69+
experimentalFeature: token.spec.experimentalFeature,
70+
experimentalFeature2: nil
6671
)
6772
}
6873
}

Sources/SwiftParser/Expressions.swift

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -118,9 +118,18 @@ extension Parser {
118118
//
119119
// Only do this if we're parsing a pattern, to improve QoI on malformed
120120
// expressions followed by (e.g.) let/var decls.
121-
if pattern != .none, self.at(anyIn: MatchingPatternStart.self) != nil {
122-
let pattern = self.parseMatchingPattern(context: .matching)
123-
return RawExprSyntax(RawPatternExprSyntax(pattern: pattern, arena: self.arena))
121+
if pattern != .none,
122+
let matchStart = self.at(anyIn: MatchingPatternStart.self) {
123+
switch matchStart.spec {
124+
case .rhs(let bindingIntroducer):
125+
var backtrack = lookahead()
126+
if backtrack.shouldParsePatternBinding(introducer: bindingIntroducer) {
127+
fallthrough
128+
}
129+
default:
130+
let pattern = self.parseMatchingPattern(context: .matching)
131+
return RawExprSyntax(RawPatternExprSyntax(pattern: pattern, arena: self.arena))
132+
}
124133
}
125134
return RawExprSyntax(self.parseSequenceExpression(flavor: flavor, pattern: pattern))
126135
}

Sources/SwiftParser/Patterns.swift

Lines changed: 57 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -85,16 +85,29 @@ extension Parser {
8585
arena: self.arena
8686
)
8787
)
88-
case (.rhs, let handle)?:
89-
let bindingSpecifier = self.eat(handle)
90-
let value = self.parsePattern()
91-
return RawPatternSyntax(
92-
RawValueBindingPatternSyntax(
93-
bindingSpecifier: bindingSpecifier,
94-
pattern: value,
95-
arena: self.arena
88+
case (.rhs(let introducer), let handle)?:
89+
var backtrack = self.lookahead()
90+
if backtrack.shouldParsePatternBinding(introducer: introducer) {
91+
let bindingSpecifier = self.eat(handle)
92+
let value = self.parsePattern()
93+
return RawPatternSyntax(
94+
RawValueBindingPatternSyntax(
95+
bindingSpecifier: bindingSpecifier,
96+
pattern: value,
97+
arena: self.arena
98+
)
9699
)
97-
)
100+
} else {
101+
// If we shouldn't contextually parse as a pattern binding introducer,
102+
// then parse as an identifier.
103+
let identifier = self.eat(handle)
104+
return RawPatternSyntax(
105+
RawIdentifierPatternSyntax(
106+
identifier: identifier,
107+
arena: self.arena
108+
)
109+
)
110+
}
98111
case nil:
99112
break
100113
}
@@ -218,16 +231,19 @@ extension Parser {
218231
arena: self.arena
219232
)
220233
)
221-
case (.rhs, let handle)?:
222-
let bindingSpecifier = self.eat(handle)
223-
let value = self.parseMatchingPattern(context: .bindingIntroducer)
224-
return RawPatternSyntax(
225-
RawValueBindingPatternSyntax(
226-
bindingSpecifier: bindingSpecifier,
227-
pattern: value,
228-
arena: self.arena
234+
case (.rhs(let introducer), let handle)?:
235+
var backtrack = lookahead()
236+
if backtrack.shouldParsePatternBinding(introducer: introducer) {
237+
let bindingSpecifier = self.eat(handle)
238+
let value = self.parseMatchingPattern(context: .bindingIntroducer)
239+
return RawPatternSyntax(
240+
RawValueBindingPatternSyntax(
241+
bindingSpecifier: bindingSpecifier,
242+
pattern: value,
243+
arena: self.arena
244+
)
229245
)
230-
)
246+
}
231247
case nil:
232248
break
233249
}
@@ -253,6 +269,21 @@ extension Parser {
253269
// MARK: Lookahead
254270

255271
extension Parser.Lookahead {
272+
/// Returns true if we should parse a pattern binding specifier contextually
273+
/// as one.
274+
mutating func shouldParsePatternBinding(introducer: ValueBindingPatternSyntax.BindingSpecifierOptions) -> Bool {
275+
switch introducer {
276+
// TODO: the other ownership modifiers (borrowing/consuming/mutating) more
277+
// than likely need to be made contextual as well before finalizing their
278+
// grammar.
279+
case ._borrowing where experimentalFeatures.contains(.borrowingSwitch):
280+
return peek(isAt: TokenSpec(.identifier, allowAtStartOfLine: false))
281+
default:
282+
// Other keywords can be parsed unconditionally.
283+
return true
284+
}
285+
}
286+
256287
/// pattern ::= identifier
257288
/// pattern ::= '_'
258289
/// pattern ::= pattern-tuple
@@ -294,9 +325,14 @@ extension Parser.Lookahead {
294325
return true
295326
case (.lhs(.leftParen), _)?:
296327
return self.canParsePatternTuple()
297-
case (.rhs, let handle)?:
298-
self.eat(handle)
299-
return self.canParsePattern()
328+
case (.rhs(let introducer), let handle)?:
329+
if shouldParsePatternBinding(introducer: introducer) {
330+
self.eat(handle)
331+
return self.canParsePattern()
332+
} else {
333+
self.eat(handle)
334+
return true
335+
}
300336
case nil:
301337
return false
302338
}

Sources/SwiftParser/generated/ExperimentalFeatures.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,7 @@ extension Parser.ExperimentalFeatures {
3838

3939
/// Whether to enable the parsing of TransferringArgsAndResults.
4040
public static let transferringArgsAndResults = Self (rawValue: 1 << 4)
41+
42+
/// Whether to enable the parsing of borrowing pattern matching.
43+
public static let borrowingSwitch = Self (rawValue: 1 << 5)
4144
}

Sources/SwiftParser/generated/Parser+TokenSpecSet.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2318,7 +2318,7 @@ extension OptionalBindingConditionSyntax {
23182318
self = .inout
23192319
case TokenSpec(._mutating) where experimentalFeatures.contains(.referenceBindings):
23202320
self = ._mutating
2321-
case TokenSpec(._borrowing) where experimentalFeatures.contains(.referenceBindings):
2321+
case TokenSpec(._borrowing) where experimentalFeatures.contains(.referenceBindings) || experimentalFeatures.contains(.borrowingSwitch):
23222322
self = ._borrowing
23232323
case TokenSpec(._consuming) where experimentalFeatures.contains(.referenceBindings):
23242324
self = ._consuming
@@ -2992,7 +2992,7 @@ extension ValueBindingPatternSyntax {
29922992
self = .inout
29932993
case TokenSpec(._mutating) where experimentalFeatures.contains(.referenceBindings):
29942994
self = ._mutating
2995-
case TokenSpec(._borrowing) where experimentalFeatures.contains(.referenceBindings):
2995+
case TokenSpec(._borrowing) where experimentalFeatures.contains(.referenceBindings) || experimentalFeatures.contains(.borrowingSwitch):
29962996
self = ._borrowing
29972997
case TokenSpec(._consuming) where experimentalFeatures.contains(.referenceBindings):
29982998
self = ._consuming
@@ -3064,7 +3064,7 @@ extension VariableDeclSyntax {
30643064
self = .inout
30653065
case TokenSpec(._mutating) where experimentalFeatures.contains(.referenceBindings):
30663066
self = ._mutating
3067-
case TokenSpec(._borrowing) where experimentalFeatures.contains(.referenceBindings):
3067+
case TokenSpec(._borrowing) where experimentalFeatures.contains(.referenceBindings) || experimentalFeatures.contains(.borrowingSwitch):
30683068
self = ._borrowing
30693069
case TokenSpec(._consuming) where experimentalFeatures.contains(.referenceBindings):
30703070
self = ._consuming

Tests/SwiftParserTest/translated/MatchingPatternsTests.swift

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -606,4 +606,102 @@ final class MatchingPatternsTests: ParserTestCase {
606606
experimentalFeatures: .referenceBindings
607607
)
608608
}
609+
610+
func testBorrowingContextualParsing() {
611+
assertParse(
612+
"""
613+
switch bar {
614+
case _borrowing .foo(): // parses as `_borrowing.foo()` as before
615+
break
616+
}
617+
""",
618+
experimentalFeatures: .borrowingSwitch
619+
)
620+
621+
assertParse(
622+
"""
623+
switch bar {
624+
case _borrowing (): // parses as `_borrowing()` as before
625+
break
626+
}
627+
""",
628+
experimentalFeatures: .borrowingSwitch
629+
)
630+
631+
assertParse(
632+
"""
633+
switch bar {
634+
case _borrowing x: // parses as binding
635+
break
636+
}
637+
""",
638+
experimentalFeatures: .borrowingSwitch
639+
)
640+
641+
assertParse(
642+
"""
643+
switch bar {
644+
case _borrowing x: // parses as binding
645+
break
646+
}
647+
""",
648+
experimentalFeatures: .borrowingSwitch
649+
)
650+
651+
assertParse(
652+
"""
653+
switch bar {
654+
case .payload(_borrowing x): // parses as binding
655+
break
656+
}
657+
""",
658+
experimentalFeatures: .borrowingSwitch
659+
)
660+
661+
assertParse(
662+
"""
663+
switch bar {
664+
case _borrowing x.member: // parses as var introducer surrounding postfix expression (which never is valid)
665+
break
666+
}
667+
""",
668+
experimentalFeatures: .borrowingSwitch
669+
)
670+
assertParse(
671+
"""
672+
switch bar {
673+
case let _borrowing: // parses as let binding named '_borrowing'
674+
break
675+
}
676+
""",
677+
experimentalFeatures: .borrowingSwitch
678+
)
679+
assertParse(
680+
"""
681+
switch bar {
682+
case _borrowing + _borrowing: // parses as expr pattern
683+
break
684+
}
685+
""",
686+
experimentalFeatures: .borrowingSwitch
687+
)
688+
assertParse(
689+
"""
690+
switch bar {
691+
case _borrowing(let _borrowing): // parses as let binding named '_borrowing' inside a case pattern named 'borrowing'
692+
break
693+
}
694+
""",
695+
experimentalFeatures: .borrowingSwitch
696+
)
697+
assertParse(
698+
"""
699+
switch bar {
700+
case foo(_borrowing + _borrowing): // parses as expr pattern
701+
break
702+
}
703+
""",
704+
experimentalFeatures: .borrowingSwitch
705+
)
706+
}
609707
}

0 commit comments

Comments
 (0)