Skip to content

Introduce ExperimentalFeatures #2041

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Aug 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion CodeGeneration/Sources/SyntaxSupport/KeywordSpec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,15 @@
//
//===----------------------------------------------------------------------===//

import SwiftSyntax

public struct KeywordSpec {
public var name: String

/// If `true`, this is for an experimental language feature, and any public
/// API generated should be SPI.
public var isExperimental: Bool

public var isLexerClassified: Bool
public var requiresLeadingSpace: Bool
public var requiresTrailingSpace: Bool
Expand All @@ -24,10 +31,18 @@ public struct KeywordSpec {
}
}

/// Retrieve the attributes that should be printed on any API for the
/// generated keyword.
public var apiAttributes: AttributeListSyntax {
guard isExperimental else { return "" }
return AttributeListSyntax("@_spi(ExperimentalLanguageFeatures)").with(\.trailingTrivia, .newline)
}

/// `isLexerClassified` determines whether the token kind is switched from being an identifier to a keyword in the lexer.
/// This is true for keywords that used to be considered non-contextual.
init(_ name: String, isLexerClassified: Bool = false, requiresLeadingSpace: Bool = false, requiresTrailingSpace: Bool = false) {
init(_ name: String, isExperimental: Bool = false, isLexerClassified: Bool = false, requiresLeadingSpace: Bool = false, requiresTrailingSpace: Bool = false) {
self.name = name
self.isExperimental = isExperimental
self.isLexerClassified = isLexerClassified
self.requiresLeadingSpace = requiresLeadingSpace
self.requiresTrailingSpace = requiresTrailingSpace
Expand Down
22 changes: 22 additions & 0 deletions CodeGeneration/Sources/SyntaxSupport/Node.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ public class Node {
/// The kind of node’s supertype. This kind must have `isBase == true`
public let base: SyntaxNodeKind

/// If `true`, this is for an experimental language feature, and any public
/// API generated should be SPI.
public let isExperimental: Bool

/// When the node name is printed for diagnostics, this name is used.
/// If `nil`, `nameForDiagnostics` will print the parent node’s name.
public let nameForDiagnostics: String?
Expand Down Expand Up @@ -85,10 +89,25 @@ public class Node {
}
}

/// Retrieve the attributes that should be printed on any API for the
/// generated node. If `forRaw` is true, this is for the raw syntax node.
public func apiAttributes(forRaw: Bool = false) -> AttributeListSyntax {
let attrList = AttributeListSyntax {
if isExperimental {
"@_spi(ExperimentalLanguageFeatures)"
}
if forRaw {
"@_spi(RawSyntax)"
}
}
return attrList.with(\.trailingTrivia, attrList.isEmpty ? [] : .newline)
}

/// Construct the specification for a layout syntax node.
init(
kind: SyntaxNodeKind,
base: SyntaxNodeKind,
isExperimental: Bool = false,
nameForDiagnostics: String?,
documentation: String? = nil,
parserFunction: String? = nil,
Expand All @@ -100,6 +119,7 @@ public class Node {

self.kind = kind
self.base = base
self.isExperimental = isExperimental
self.nameForDiagnostics = nameForDiagnostics
self.documentation = docCommentTrivia(from: documentation)
self.parserFunction = parserFunction
Expand Down Expand Up @@ -204,6 +224,7 @@ public class Node {
init(
kind: SyntaxNodeKind,
base: SyntaxNodeKind,
isExperimental: Bool = false,
nameForDiagnostics: String?,
documentation: String? = nil,
parserFunction: String? = nil,
Expand All @@ -212,6 +233,7 @@ public class Node {
self.kind = kind
precondition(base == .syntaxCollection)
self.base = base
self.isExperimental = isExperimental
self.nameForDiagnostics = nameForDiagnostics
self.documentation = docCommentTrivia(from: documentation)
self.parserFunction = parserFunction
Expand Down
14 changes: 14 additions & 0 deletions CodeGeneration/Sources/SyntaxSupport/TokenSpec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,22 +22,36 @@ public struct TokenSpec {
}

public let varOrCaseName: TokenSyntax

/// If `true`, this is for an experimental language feature, and any public
/// API generated should be SPI.
public let isExperimental: Bool

public let nameForDiagnostics: String
public let text: String?
public let kind: Kind

fileprivate init(
name: String,
isExperimental: Bool = false,
nameForDiagnostics: String,
text: String? = nil,
kind: Kind
) {
self.varOrCaseName = .identifier(name)
self.isExperimental = isExperimental
self.nameForDiagnostics = nameForDiagnostics
self.text = text
self.kind = kind
}

/// Retrieve the attributes that should be printed on any API for the
/// generated token.
public var apiAttributes: AttributeListSyntax {
guard isExperimental else { return "" }
return AttributeListSyntax("@_spi(ExperimentalLanguageFeatures)").with(\.trailingTrivia, .newline)
}

static func punctuator(name: String, text: String) -> TokenSpec {
return TokenSpec(
name: name,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import SyntaxSupport
import Utils

let syntaxKindNameForDiagnosticFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
DeclSyntax("import SwiftSyntax")
DeclSyntax("@_spi(ExperimentalLanguageFeatures) import SwiftSyntax")

try! ExtensionDeclSyntax("extension SyntaxKind") {
try VariableDeclSyntax("var nameForDiagnostics: String?") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,12 @@ let keywordFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
"""
) {
for (index, keyword) in KEYWORDS.enumerated() {
DeclSyntax("case \(raw: keyword.escapedName)")
DeclSyntax(
"""
\(keyword.apiAttributes)\
case \(raw: keyword.escapedName)
"""
)
}

try! InitializerDeclSyntax("@_spi(RawSyntax) public init?(_ text: SyntaxText)") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ let rawSyntaxNodesFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
for node in SYNTAX_NODES where node.kind.isBase {
DeclSyntax(
"""
@_spi(RawSyntax)
\(node.apiAttributes(forRaw: true))\
public protocol \(node.kind.rawType)NodeProtocol: \(raw: node.base.rawProtocolType) {}
"""
)
Expand All @@ -51,7 +51,7 @@ let rawSyntaxNodesFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
for node in SYNTAX_NODES {
try! StructDeclSyntax(
"""
@_spi(RawSyntax)
\(node.apiAttributes(forRaw: true))\
public struct \(node.kind.rawType): \(node.kind.isBase ? node.kind.rawProtocolType : node.base.rawProtocolType)
"""
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ let nodesSections: String = {

for (baseKind, heading) in nodeKinds {
let baseTypes = ["\(baseKind.syntaxType)", "\(baseKind.syntaxType)Protocol", "Missing\(baseKind.syntaxType)"]
let leafTypes = SYNTAX_NODES.filter({ $0.base == baseKind && !$0.kind.isMissing }).map(\.kind.syntaxType.description)
let leafTypes = SYNTAX_NODES.filter({ $0.base == baseKind && !$0.kind.isMissing && !$0.isExperimental }).map(\.kind.syntaxType.description)
addSection(heading: heading, types: baseTypes + leafTypes)
}

Expand All @@ -48,7 +48,7 @@ let nodesSections: String = {
"SyntaxChildrenIndex",
]
+ SYNTAX_NODES.flatMap({ (node: Node) -> [String] in
guard let node = node.collectionNode else {
guard let node = node.collectionNode, !node.isExperimental else {
return []
}
return [node.kind.syntaxType.description]
Expand All @@ -59,9 +59,12 @@ let nodesSections: String = {
})
)

addSection(heading: "Attributes", types: ATTRIBUTE_NODES.map(\.kind.syntaxType.description).sorted())
addSection(heading: "Attributes", types: ATTRIBUTE_NODES.filter({ !$0.isExperimental }).map(\.kind.syntaxType.description).sorted())

addSection(heading: "Miscellaneous Syntax", types: SYNTAX_NODES.map(\.kind.syntaxType.description).filter({ !handledSyntaxTypes.contains($0) }))
addSection(
heading: "Miscellaneous Syntax",
types: SYNTAX_NODES.filter({ !$0.isExperimental }).map(\.kind.syntaxType.description).filter({ !handledSyntaxTypes.contains($0) })
)

addSection(heading: "Traits", types: TRAITS.map { "\($0.protocolName)" })

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ let syntaxAnyVisitorFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
for node in SYNTAX_NODES where !node.kind.isBase {
DeclSyntax(
"""
\(node.apiAttributes())\
override open func visit(_ node: \(node.kind.syntaxType)) -> SyntaxVisitorContinueKind {
return visitAny(node._syntaxNode)
}
Expand All @@ -87,6 +88,7 @@ let syntaxAnyVisitorFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {

DeclSyntax(
"""
\(node.apiAttributes())\
override open func visitPost(_ node: \(node.kind.syntaxType)) {
visitAnyPost(node._syntaxNode)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ let syntaxBaseNodesFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
/// Extension point to add common methods to all ``\(node.kind.syntaxType)`` nodes.
///
/// - Warning: Do not conform to this protocol yourself.
\(node.apiAttributes())\
public protocol \(node.kind.protocolType): \(raw: node.base.protocolType) {}
"""
)
Expand Down Expand Up @@ -59,6 +60,7 @@ let syntaxBaseNodesFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
try! StructDeclSyntax(
"""
\(node.documentation)
\(node.apiAttributes())\
public struct \(node.kind.syntaxType): \(node.kind.protocolType), SyntaxHashable
"""
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ let syntaxCollectionsFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
try! StructDeclSyntax(
"""
\(documentation)
\(node.node.apiAttributes())\
public struct \(node.kind.syntaxType): SyntaxCollection, SyntaxHashable
"""
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,12 @@ let syntaxEnumFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
) {
DeclSyntax("case token(TokenSyntax)")
for node in NON_BASE_SYNTAX_NODES {
DeclSyntax("case \(node.varOrCaseName)(\(node.kind.syntaxType))")
DeclSyntax(
"""
\(node.apiAttributes())\
case \(node.varOrCaseName)(\(node.kind.syntaxType))
"""
)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,12 @@ let syntaxKindFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
) {
DeclSyntax("case token")
for node in NON_BASE_SYNTAX_NODES {
DeclSyntax("case \(node.varOrCaseName)")
DeclSyntax(
"""
\(node.apiAttributes())\
case \(node.varOrCaseName)
"""
)
}

try VariableDeclSyntax("public var isSyntaxCollection: Bool") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ func syntaxNode(emitKind: SyntaxNodeKind) -> SourceFileSyntax {
// MARK: - \(raw: node.kind.syntaxType)

\(documentation)
\(node.node.apiAttributes())\
public struct \(raw: node.kind.syntaxType): \(raw: node.baseType.syntaxBaseName)Protocol, SyntaxHashable
"""
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ let syntaxRewriterFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
/// Visit a ``\(node.kind.syntaxType)``.
/// - Parameter node: the node that is being visited
/// - Returns: the rewritten node
\(node.apiAttributes())\
open func visit(_ node: \(node.kind.syntaxType)) -> \(node.kind.syntaxType) {
return Syntax(visitChildren(node)).cast(\(node.kind.syntaxType).self)
}
Expand All @@ -136,6 +137,7 @@ let syntaxRewriterFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
/// Visit a ``\(node.kind.syntaxType)``.
/// - Parameter node: the node that is being visited
/// - Returns: the rewritten node
\(node.apiAttributes())\
open func visit(_ node: \(node.kind.syntaxType)) -> \(raw: node.baseType.syntaxBaseName) {
return \(raw: node.baseType.syntaxBaseName)(visitChildren(node))
}
Expand All @@ -144,12 +146,14 @@ let syntaxRewriterFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
}
}

for baseKind in SyntaxNodeKind.allCases where baseKind.isBase && baseKind != .syntax && baseKind != .syntaxCollection {
for baseNode in SYNTAX_NODES where baseNode.kind.isBase && baseNode.kind != .syntax && baseNode.kind != .syntaxCollection {
let baseKind = baseNode.kind
DeclSyntax(
"""
/// Visit any \(raw: baseKind.syntaxType) node.
/// - Parameter node: the node that is being visited
/// - Returns: the rewritten node
\(baseNode.apiAttributes())\
public func visit(_ node: \(raw: baseKind.syntaxType)) -> \(raw: baseKind.syntaxType) {
return visit(node.data).cast(\(raw: baseKind.syntaxType).self)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,21 @@ import SyntaxSupport
import Utils

let syntaxTransformFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
try! ProtocolDeclSyntax("public protocol SyntaxTransformVisitor") {
try! ProtocolDeclSyntax(
"""
@_spi(SyntaxTransformVisitor)
public protocol SyntaxTransformVisitor
"""
) {
DeclSyntax("associatedtype ResultType = Void")

DeclSyntax("func visitAny(_ node: Syntax) -> ResultType")

DeclSyntax("func visit(_ token: TokenSyntax) -> ResultType")

for node in SYNTAX_NODES where !node.kind.isBase {
// Don't bother including experimental nodes here since we want to remove
// SyntaxTransformVisitor anyway.
for node in SYNTAX_NODES where !node.kind.isBase && !node.isExperimental {
DeclSyntax(
"""
/// Visiting ``\(node.kind.syntaxType)`` specifically.
Expand All @@ -50,6 +57,7 @@ let syntaxTransformFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
/// Visiting ``\(node.kind.syntaxType)`` specifically.
/// - Parameter node: the node we are visiting.
/// - Returns: nil by default.
\(node.apiAttributes())\
public func visit(_ node: \(node.kind.syntaxType)) -> ResultType {
visitAny(Syntax(node))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ let syntaxVisitorFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
/// Visiting ``\(node.kind.syntaxType)`` specifically.
/// - Parameter node: the node we are visiting.
/// - Returns: how should we continue visiting.
\(node.apiAttributes())\
open func visit(_ node: \(node.kind.syntaxType)) -> SyntaxVisitorContinueKind {
return .visitChildren
}
Expand All @@ -66,6 +67,7 @@ let syntaxVisitorFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
"""
/// The function called after visiting ``\(node.kind.syntaxType)`` and its descendants.
/// - node: the node we just finished visiting.
\(node.apiAttributes())\
open func visitPost(_ node: \(node.kind.syntaxType)) {}
"""
)
Expand Down
Loading