Skip to content

Commit a99853b

Browse files
committed
Teach TokenSpecSet about experimental features
Pass down the current set of experimental features to TokenSpecSets, and have generated TokenSpecSets automatically account for experimental features by returning `nil` if a given feature isn't active.
1 parent 7ef269f commit a99853b

22 files changed

+209
-103
lines changed
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2023 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import SwiftSyntax
14+
15+
public enum ExperimentalFeature: String, CaseIterable {
16+
case referenceBindings
17+
case thenStatements
18+
19+
/// The name of the feature, which is used in the doc comment.
20+
public var featureName: String {
21+
switch self {
22+
case .referenceBindings:
23+
return "reference bindings"
24+
case .thenStatements:
25+
return "'then' statements"
26+
}
27+
}
28+
29+
/// The token that represents the experimental feature case name.
30+
public var token: TokenSyntax {
31+
.identifier(rawValue)
32+
}
33+
}

CodeGeneration/Sources/SyntaxSupport/KeywordSpec.swift

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,18 @@ public struct KeywordSpec {
1616
/// The name of the keyword.
1717
public let name: String
1818

19+
/// The experimental feature the keyword is part of, or `nil` if this isn't
20+
/// for an experimental feature.
21+
public let experimentalFeature: ExperimentalFeature?
22+
23+
/// Indicates if the token kind is switched from being an identifier to a keyword in the lexer.
24+
public let isLexerClassified: Bool
25+
1926
/// Indicates if the keyword is part of an experimental language feature.
2027
///
2128
/// If `true`, this keyword is for an experimental language feature, and any public
2229
/// API generated should be marked as SPI
23-
public let isExperimental: Bool
24-
25-
/// Indicates if the token kind is switched from being an identifier to a keyword in the lexer.
26-
public let isLexerClassified: Bool
30+
public var isExperimental: Bool { experimentalFeature != nil }
2731

2832
/// The name of this keyword that's suitable to be used for variable or enum case names.
2933
public var varOrCaseName: TokenSyntax {
@@ -46,15 +50,15 @@ public struct KeywordSpec {
4650
///
4751
/// - Parameters:
4852
/// - name: A name of the keyword.
49-
/// - isExperimental: Indicates if the keyword is part of an experimental language feature.
53+
/// - experimentalFeature: The experimental feature the keyword is part of, or `nil` if this isn't for an experimental feature.
5054
/// - isLexerClassified: Indicates if the token kind is switched from being an identifier to a keyword in the lexer.
5155
init(
5256
_ name: String,
53-
isExperimental: Bool = false,
57+
experimentalFeature: ExperimentalFeature? = nil,
5458
isLexerClassified: Bool = false
5559
) {
5660
self.name = name
57-
self.isExperimental = isExperimental
61+
self.experimentalFeature = experimentalFeature
5862
self.isLexerClassified = isLexerClassified
5963
}
6064
}
@@ -692,11 +696,11 @@ public enum Keyword: CaseIterable {
692696
case .yield:
693697
return KeywordSpec("yield")
694698
case ._borrowing:
695-
return KeywordSpec("_borrowing", isExperimental: true)
699+
return KeywordSpec("_borrowing", experimentalFeature: .referenceBindings)
696700
case ._consuming:
697-
return KeywordSpec("_consuming", isExperimental: true)
701+
return KeywordSpec("_consuming", experimentalFeature: .referenceBindings)
698702
case ._mutating:
699-
return KeywordSpec("_mutating", isExperimental: true)
703+
return KeywordSpec("_mutating", experimentalFeature: .referenceBindings)
700704
}
701705
}
702706
}

CodeGeneration/Sources/SyntaxSupport/Node.swift

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,9 @@ public class Node {
4040
/// The kind of node’s supertype. This kind must have `isBase == true`
4141
public let base: SyntaxNodeKind
4242

43-
/// If `true`, this is for an experimental language feature, and any public
44-
/// API generated should be SPI.
45-
public let isExperimental: Bool
43+
/// The experimental feature the node is part of, or `nil` if this isn't
44+
/// for an experimental feature.
45+
public let experimentalFeature: ExperimentalFeature?
4646

4747
/// When the node name is printed for diagnostics, this name is used.
4848
/// If `nil`, `nameForDiagnostics` will print the parent node’s name.
@@ -57,6 +57,10 @@ public class Node {
5757
/// function that should be invoked to create this node.
5858
public let parserFunction: TokenSyntax?
5959

60+
/// If `true`, this is for an experimental language feature, and any public
61+
/// API generated should be SPI.
62+
public var isExperimental: Bool { experimentalFeature != nil }
63+
6064
/// A name for this node that is suitable to be used as a variables or enum
6165
/// case's name.
6266
public var varOrCaseName: TokenSyntax {
@@ -107,7 +111,7 @@ public class Node {
107111
init(
108112
kind: SyntaxNodeKind,
109113
base: SyntaxNodeKind,
110-
isExperimental: Bool = false,
114+
experimentalFeature: ExperimentalFeature? = nil,
111115
nameForDiagnostics: String?,
112116
documentation: String? = nil,
113117
parserFunction: TokenSyntax? = nil,
@@ -119,7 +123,7 @@ public class Node {
119123

120124
self.kind = kind
121125
self.base = base
122-
self.isExperimental = isExperimental
126+
self.experimentalFeature = experimentalFeature
123127
self.nameForDiagnostics = nameForDiagnostics
124128
self.documentation = docCommentTrivia(from: documentation)
125129
self.parserFunction = parserFunction
@@ -226,7 +230,7 @@ public class Node {
226230
init(
227231
kind: SyntaxNodeKind,
228232
base: SyntaxNodeKind,
229-
isExperimental: Bool = false,
233+
experimentalFeature: ExperimentalFeature? = nil,
230234
nameForDiagnostics: String?,
231235
documentation: String? = nil,
232236
parserFunction: TokenSyntax? = nil,
@@ -235,7 +239,7 @@ public class Node {
235239
self.kind = kind
236240
precondition(base == .syntaxCollection)
237241
self.base = base
238-
self.isExperimental = isExperimental
242+
self.experimentalFeature = experimentalFeature
239243
self.nameForDiagnostics = nameForDiagnostics
240244
self.documentation = docCommentTrivia(from: documentation)
241245
self.parserFunction = parserFunction

CodeGeneration/Sources/SyntaxSupport/StmtNodes.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -588,7 +588,7 @@ public let STMT_NODES: [Node] = [
588588
kind: .thenStmt,
589589
base: .stmt,
590590
// FIXME: This should be marked experimental.
591-
isExperimental: false,
591+
experimentalFeature: nil,
592592
nameForDiagnostics: "'then' statement",
593593
documentation: """
594594
A statement used to indicate the produced value from an if/switch

CodeGeneration/Sources/SyntaxSupport/TokenSpec.swift

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,9 @@ public struct TokenSpec {
2424
/// The name of the token, suitable for use in variable or enum case names.
2525
public let varOrCaseName: TokenSyntax
2626

27-
/// Indicates if the token is part of an experimental language feature.
28-
///
29-
/// If `true`, this keyword is for an experimental language feature, and any public
30-
/// API generated should be marked as SPI
31-
public let isExperimental: Bool
27+
/// The experimental feature the token is part of, or `nil` if this isn't
28+
/// for an experimental feature.
29+
public let experimentalFeature: ExperimentalFeature?
3230

3331
/// The name of the token that can be shown in diagnostics.
3432
public let nameForDiagnostics: String
@@ -39,6 +37,12 @@ public struct TokenSpec {
3937
/// The kind of the token.
4038
public let kind: Kind
4139

40+
/// Indicates if the token is part of an experimental language feature.
41+
///
42+
/// If `true`, this keyword is for an experimental language feature, and any public
43+
/// API generated should be marked as SPI
44+
public var isExperimental: Bool { experimentalFeature != nil }
45+
4246
/// The attributes that should be printed on any API for the generated keyword.
4347
///
4448
/// This is typically used to mark APIs as SPI when the keyword is part of an experimental language feature.
@@ -51,19 +55,19 @@ public struct TokenSpec {
5155
///
5256
/// - Parameters:
5357
/// - name: A name of the token.
54-
/// - isExperimental: Indicates if the token is part of an experimental language feature.
58+
/// - experimentalFeature: The experimental feature the token is part of, or `nil` if this isn't for an experimental feature.
5559
/// - nameForDiagnostics: A name of the token that can be shown in diagnostics.
5660
/// - text: An actual text of the token, if available.
5761
/// - kind: A kind of the token.
5862
fileprivate init(
5963
name: String,
60-
isExperimental: Bool = false,
64+
experimentalFeature: ExperimentalFeature? = nil,
6165
nameForDiagnostics: String,
6266
text: String? = nil,
6367
kind: Kind
6468
) {
6569
self.varOrCaseName = .identifier(name)
66-
self.isExperimental = isExperimental
70+
self.experimentalFeature = experimentalFeature
6771
self.nameForDiagnostics = nameForDiagnostics
6872
self.text = text
6973
self.kind = kind

CodeGeneration/Sources/generate-swift-syntax/GenerateSwiftSyntax.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ struct GenerateSwiftSyntax: ParsableCommand {
8888

8989
var fileSpecs: [GeneratedFileSpec] = [
9090
// SwiftParser
91+
GeneratedFileSpec(swiftParserGeneratedDir + ["ExperimentalFeatures.swift"], experimentalFeaturesFile),
9192
GeneratedFileSpec(swiftParserGeneratedDir + ["IsLexerClassified.swift"], isLexerClassifiedFile),
9293
GeneratedFileSpec(swiftParserGeneratedDir + ["LayoutNodes+Parsable.swift"], layoutNodesParsableFile),
9394
GeneratedFileSpec(swiftParserGeneratedDir + ["Parser+TokenSpecSet.swift"], parserTokenSpecSetFile),
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2023 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import SwiftSyntax
14+
import SwiftSyntaxBuilder
15+
import SyntaxSupport
16+
import Utils
17+
18+
let experimentalFeaturesFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
19+
DeclSyntax(
20+
"""
21+
extension Parser {
22+
@_spi(ExperimentalLanguageFeatures)
23+
public struct ExperimentalFeatures: OptionSet {
24+
public let rawValue: UInt
25+
public init(rawValue: UInt) {
26+
self.rawValue = rawValue
27+
}
28+
}
29+
}
30+
"""
31+
)
32+
33+
try! ExtensionDeclSyntax("extension Parser.ExperimentalFeatures") {
34+
for (i, feature) in ExperimentalFeature.allCases.enumerated() {
35+
DeclSyntax(
36+
"""
37+
/// Whether to enable the parsing of \(raw: feature.featureName).
38+
public static let \(feature.token) = Self(rawValue: 1 << \(raw: i))
39+
"""
40+
)
41+
}
42+
}
43+
}

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

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +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+
} ?? ""
23+
return "case TokenSpec(.\(caseName))\(raw: whereClause): self = .\(caseName)"
24+
}
25+
1826
let parserTokenSpecSetFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
1927
DeclSyntax("@_spi(RawSyntax) @_spi(ExperimentalLanguageFeatures) import SwiftSyntax")
2028

@@ -42,16 +50,20 @@ let parserTokenSpecSetFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
4250
}
4351
}
4452

45-
try InitializerDeclSyntax("init?(lexeme: Lexer.Lexeme)") {
53+
try InitializerDeclSyntax("init?(lexeme: Lexer.Lexeme, experimentalFeatures: Parser.ExperimentalFeatures)") {
4654
try SwitchExprSyntax("switch PrepareForKeywordMatch(lexeme)") {
4755
for choice in choices {
4856
switch choice {
4957
case .keyword(let keyword):
50-
let caseName = keyword.spec.varOrCaseName
51-
SwitchCaseSyntax("case TokenSpec(.\(caseName)): self = .\(caseName)")
58+
tokenCaseMatch(
59+
keyword.spec.varOrCaseName,
60+
experimentalFeature: keyword.spec.experimentalFeature
61+
)
5262
case .token(let token):
53-
let caseName = token.spec.varOrCaseName
54-
SwitchCaseSyntax("case TokenSpec(.\(caseName)): self = .\(caseName)")
63+
tokenCaseMatch(
64+
token.spec.varOrCaseName,
65+
experimentalFeature: token.spec.experimentalFeature
66+
)
5567
}
5668
}
5769
SwitchCaseSyntax("default: return nil")

Sources/SwiftParser/Attributes.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ extension Parser {
6464
case objc
6565
case transpose
6666

67-
init?(lexeme: Lexer.Lexeme) {
67+
init?(lexeme: Lexer.Lexeme, experimentalFeatures: Parser.ExperimentalFeatures) {
6868
switch PrepareForKeywordMatch(lexeme) {
6969
case TokenSpec(._alignment): self = ._alignment
7070
case TokenSpec(._backDeploy): self = ._backDeploy
@@ -1043,7 +1043,7 @@ extension Parser {
10431043
}
10441044
}
10451045

1046-
init?(lexeme: Lexer.Lexeme) {
1046+
init?(lexeme: Lexer.Lexeme, experimentalFeatures: Parser.ExperimentalFeatures) {
10471047
switch PrepareForKeywordMatch(lexeme) {
10481048
case TokenSpec(.private): self = .private
10491049
case TokenSpec(.fileprivate): self = .fileprivate

Sources/SwiftParser/Availability.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ extension Parser {
6060
case star
6161
case identifier
6262

63-
init?(lexeme: Lexer.Lexeme) {
63+
init?(lexeme: Lexer.Lexeme, experimentalFeatures: Parser.ExperimentalFeatures) {
6464
switch PrepareForKeywordMatch(lexeme) {
6565
case TokenSpec(.message): self = .message
6666
case TokenSpec(.renamed): self = .renamed

Sources/SwiftParser/Declarations.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -521,7 +521,7 @@ extension Parser {
521521
case postfixOperator
522522
case prefixOperator
523523

524-
init?(lexeme: Lexer.Lexeme) {
524+
init?(lexeme: Lexer.Lexeme, experimentalFeatures: Parser.ExperimentalFeatures) {
525525
switch (lexeme.rawTokenKind, lexeme.tokenText) {
526526
case (.colon, _): self = .colon
527527
case (.binaryOperator, "=="): self = .binaryOperator
@@ -1719,7 +1719,7 @@ extension Parser {
17191719
case higherThan
17201720
case lowerThan
17211721

1722-
init?(lexeme: Lexer.Lexeme) {
1722+
init?(lexeme: Lexer.Lexeme, experimentalFeatures: Parser.ExperimentalFeatures) {
17231723
switch PrepareForKeywordMatch(lexeme) {
17241724
case TokenSpec(.associativity): self = .associativity
17251725
case TokenSpec(.assignment): self = .assignment

Sources/SwiftParser/Directives.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ extension Parser {
2626
}
2727
}
2828

29-
init?(lexeme: Lexer.Lexeme) {
29+
init?(lexeme: Lexer.Lexeme, experimentalFeatures: Parser.ExperimentalFeatures) {
3030
switch PrepareForKeywordMatch(lexeme) {
3131
case TokenSpec(.poundElseif): self = .poundElseif
3232
case TokenSpec(.poundElse): self = .poundElse

Sources/SwiftParser/Expressions.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,7 @@ extension Parser {
244244
case arrow
245245
case `throws`
246246

247-
init?(lexeme: Lexer.Lexeme) {
247+
init?(lexeme: Lexer.Lexeme, experimentalFeatures: Parser.ExperimentalFeatures) {
248248
switch PrepareForKeywordMatch(lexeme) {
249249
case TokenSpec(.binaryOperator): self = .binaryOperator
250250
case TokenSpec(.infixQuestionMark): self = .infixQuestionMark

Sources/SwiftParser/Lookahead.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -333,7 +333,7 @@ extension Parser.Lookahead {
333333
case poundElse
334334
case poundElseif
335335

336-
init?(lexeme: Lexer.Lexeme) {
336+
init?(lexeme: Lexer.Lexeme, experimentalFeatures: Parser.ExperimentalFeatures) {
337337
switch lexeme.rawTokenKind {
338338
case .leftParen: self = .leftParen
339339
case .leftBrace: self = .leftBrace

Sources/SwiftParser/Modifiers.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ extension Parser {
169169
}
170170
}
171171

172-
init?(lexeme: Lexer.Lexeme) {
172+
init?(lexeme: Lexer.Lexeme, experimentalFeatures: Parser.ExperimentalFeatures) {
173173
switch PrepareForKeywordMatch(lexeme) {
174174
case TokenSpec(.private): self = .private
175175
case TokenSpec(.fileprivate): self = .fileprivate

Sources/SwiftParser/Names.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,9 @@ extension Lexer.Lexeme {
304304
}
305305

306306
func isContextualPunctuator(_ name: SyntaxText) -> Bool {
307-
return Operator(lexeme: self) != nil && self.tokenText == name
307+
// Currently we can ignore experimental features since a new kind of
308+
// non-prefix/infix/postfix operator seems unlikely.
309+
return Operator(lexeme: self, experimentalFeatures: []) != nil && self.tokenText == name
308310
}
309311

310312
var isLexerClassifiedKeyword: Bool {

0 commit comments

Comments
 (0)