Skip to content
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

Partial SwiftSyntax support for experimental @abi attribute #2902

Merged
merged 2 commits into from
Dec 4, 2024
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
29 changes: 29 additions & 0 deletions CodeGeneration/Sources/SyntaxSupport/AttributeNodes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,11 @@ public let ATTRIBUTE_NODES: [Node] = [
name: "documentationArguments",
kind: .node(kind: .documentationAttributeArgumentList)
),
Child(
name: "abiArguments",
kind: .node(kind: .abiAttributeArguments),
experimentalFeature: .abiAttribute
),
]),
documentation: """
The arguments of the attribute.
Expand Down Expand Up @@ -267,6 +272,30 @@ public let ATTRIBUTE_NODES: [Node] = [
]
),

Node(
kind: .abiAttributeArguments,
base: .syntax,
experimentalFeature: .abiAttribute,
nameForDiagnostics: "ABI-providing declaration",
documentation: "The arguments of the '@abi' attribute",
children: [
Child(
name: "provider",
kind: .nodeChoices(choices: [
Child(name: "associatedType", kind: .node(kind: .associatedTypeDecl)),
Child(name: "deinitializer", kind: .node(kind: .deinitializerDecl)),
Child(name: "enumCase", kind: .node(kind: .enumCaseDecl)),
Child(name: "function", kind: .node(kind: .functionDecl)),
Child(name: "initializer", kind: .node(kind: .initializerDecl)),
Child(name: "missing", kind: .node(kind: .missingDecl)),
Child(name: "subscript", kind: .node(kind: .subscriptDecl)),
Child(name: "typeAlias", kind: .node(kind: .typeAliasDecl)),
Child(name: "variable", kind: .node(kind: .variableDecl)),
])
)
]
),

Node(
kind: .conventionAttributeArguments,
base: .syntax,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ public enum ExperimentalFeature: String, CaseIterable {
case trailingComma
case coroutineAccessors
case valueGenerics
case abiAttribute

/// The name of the feature as it is written in the compiler's `Features.def` file.
public var featureName: String {
Expand All @@ -38,6 +39,8 @@ public enum ExperimentalFeature: String, CaseIterable {
return "CoroutineAccessors"
case .valueGenerics:
return "ValueGenerics"
case .abiAttribute:
return "ABIAttribute"
}
}

Expand All @@ -58,6 +61,8 @@ public enum ExperimentalFeature: String, CaseIterable {
return "coroutine accessors"
case .valueGenerics:
return "value generics"
case .abiAttribute:
return "@abi attribute"
}
}

Expand Down
3 changes: 3 additions & 0 deletions CodeGeneration/Sources/SyntaxSupport/KeywordSpec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ public enum Keyword: CaseIterable {
case _underlyingVersion
case _UnknownLayout
case _version
case abi
case accesses
case actor
case addressWithNativeOwner
Expand Down Expand Up @@ -405,6 +406,8 @@ public enum Keyword: CaseIterable {
return KeywordSpec("_UnknownLayout")
case ._version:
return KeywordSpec("_version")
case .abi:
return KeywordSpec("abi", experimentalFeature: .abiAttribute)
case .accesses:
return KeywordSpec("accesses")
case .actor:
Expand Down
12 changes: 11 additions & 1 deletion CodeGeneration/Sources/SyntaxSupport/SyntaxNodeKind.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public enum SyntaxNodeKind: String, CaseIterable, IdentifierConvertible, TypeCon

case _canImportExpr
case _canImportVersionInfo
case abiAttributeArguments
case accessorBlock
case accessorDecl
case accessorDeclList
Expand Down Expand Up @@ -340,14 +341,23 @@ public enum SyntaxNodeKind: String, CaseIterable, IdentifierConvertible, TypeCon
return .identifier(rawValue)
}

public var uppercasedFirstWordRawValue: String {
switch self {
case .abiAttributeArguments:
"ABIAttributeArguments"
default:
rawValue.withFirstCharacterUppercased
}
}

public var syntaxType: TypeSyntax {
switch self {
case .syntax:
return "Syntax"
case .syntaxCollection:
return "SyntaxCollection"
default:
return "\(raw: rawValue.withFirstCharacterUppercased)Syntax"
return "\(raw: uppercasedFirstWordRawValue)Syntax"
}
}

Expand Down
2 changes: 1 addition & 1 deletion CodeGeneration/Sources/Utils/SyntaxBuildableType.swift
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ public struct SyntaxBuildableType: Hashable {
public var resultBuilderType: TypeSyntax {
switch kind {
case .node(kind: let kind):
return TypeSyntax("\(raw: kind.rawValue.withFirstCharacterUppercased)Builder")
return TypeSyntax("\(raw: kind.uppercasedFirstWordRawValue)Builder")
case .token:
preconditionFailure("Tokens cannot be constructed using result builders")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ let syntaxEnumFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {

for base in SYNTAX_NODES where base.kind.isBase {
let baseKind = base.kind
let baseName = baseKind.rawValue.withFirstCharacterUppercased
let baseName = baseKind.uppercasedFirstWordRawValue
let enumType: TypeSyntax = "\(raw: baseName)SyntaxEnum"

try! EnumDeclSyntax(
Expand Down
75 changes: 71 additions & 4 deletions Sources/SwiftParser/Attributes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@
//===----------------------------------------------------------------------===//

#if swift(>=6)
@_spi(RawSyntax) internal import SwiftSyntax
@_spi(ExperimentalLanguageFeatures) @_spi(RawSyntax) internal import SwiftSyntax
#else
@_spi(RawSyntax) import SwiftSyntax
@_spi(ExperimentalLanguageFeatures) @_spi(RawSyntax) import SwiftSyntax
#endif

extension Parser {
Expand Down Expand Up @@ -58,6 +58,7 @@ extension Parser {
case _typeEraser
case _unavailableFromAsync
case `rethrows`
case abi
case attached
case available
case backDeployed
Expand Down Expand Up @@ -95,6 +96,7 @@ extension Parser {
case TokenSpec(._typeEraser): self = ._typeEraser
case TokenSpec(._unavailableFromAsync): self = ._unavailableFromAsync
case TokenSpec(.`rethrows`): self = .rethrows
case TokenSpec(.abi) where experimentalFeatures.contains(.abiAttribute): self = .abi
case TokenSpec(.attached): self = .attached
case TokenSpec(.available): self = .available
case TokenSpec(.backDeployed): self = .backDeployed
Expand Down Expand Up @@ -136,6 +138,7 @@ extension Parser {
case ._typeEraser: return .keyword(._typeEraser)
case ._unavailableFromAsync: return .keyword(._unavailableFromAsync)
case .`rethrows`: return .keyword(.rethrows)
case .abi: return .keyword(.abi)
case .attached: return .keyword(.attached)
case .available: return .keyword(.available)
case .backDeployed: return .keyword(.backDeployed)
Expand Down Expand Up @@ -176,9 +179,16 @@ extension Parser {
case noArgument
}

/// Parse the argument of an attribute, if it has one.
///
/// - Parameters:
/// - argumentMode: Indicates whether the attribute must, may, or may not have an argument.
/// - parseArguments: Called to parse the argument list. If there is an opening parenthesis, it will have already been consumed.
/// - parseMissingArguments: If provided, called instead of `parseArgument` when an argument list was required but no opening parenthesis was present.
mutating func parseAttribute(
argumentMode: AttributeArgumentMode,
parseArguments: (inout Parser) -> RawAttributeSyntax.Arguments
parseArguments: (inout Parser) -> RawAttributeSyntax.Arguments,
parseMissingArguments: ((inout Parser) -> RawAttributeSyntax.Arguments)? = nil
) -> RawAttributeListSyntax.Element {
var (unexpectedBeforeAtSign, atSign) = self.expect(.atSign)
if atSign.trailingTriviaByteLength > 0 || self.currentToken.leadingTriviaByteLength > 0 {
Expand Down Expand Up @@ -213,7 +223,12 @@ extension Parser {
)
leftParen = leftParen.tokenView.withTokenDiagnostic(tokenDiagnostic: diagnostic, arena: self.arena)
}
let argument = parseArguments(&self)
let argument: RawAttributeSyntax.Arguments
if let parseMissingArguments, leftParen.presence == .missing {
argument = parseMissingArguments(&self)
} else {
argument = parseArguments(&self)
}
let (unexpectedBeforeRightParen, rightParen) = self.expect(.rightParen)
return .attribute(
RawAttributeSyntax(
Expand Down Expand Up @@ -255,6 +270,12 @@ extension Parser {
}

switch peek(isAtAnyIn: DeclarationAttributeWithSpecialSyntax.self) {
case .abi:
return parseAttribute(argumentMode: .required) { parser in
return .abiArguments(parser.parseABIAttributeArguments())
} parseMissingArguments: { parser in
return .abiArguments(parser.parseABIAttributeArguments(missingLParen: true))
}
case .available, ._spi_available:
return parseAttribute(argumentMode: .required) { parser in
return .availability(parser.parseAvailabilityArgumentSpecList())
Expand Down Expand Up @@ -918,6 +939,52 @@ extension Parser {
}
}

extension Parser {
/// Parse the arguments inside an `@abi(...)` attribute.
///
/// - Parameter missingLParen: `true` if the opening paren for the argument list was missing.
mutating func parseABIAttributeArguments(missingLParen: Bool = false) -> RawABIAttributeArgumentsSyntax {
func makeMissingProviderArguments(unexpectedBefore: [RawSyntax]) -> RawABIAttributeArgumentsSyntax {
return RawABIAttributeArgumentsSyntax(
provider: .missing(
RawMissingDeclSyntax(
unexpectedBefore.isEmpty ? nil : RawUnexpectedNodesSyntax(elements: unexpectedBefore, arena: self.arena),
attributes: self.emptyCollection(RawAttributeListSyntax.self),
modifiers: self.emptyCollection(RawDeclModifierListSyntax.self),
placeholder: self.missingToken(.identifier, text: "<#declaration#>"),
arena: arena
)
),
arena: self.arena
)
}

// Consider the three kinds of mistakes we might see here:
//
// 1. The user forgot the argument: `@abi(<<here>>) var x: Int`
// 2. The user forgot the left paren: `@abi<<here>> var x_abi: Int) var x: Int`
// 3. The user forgot the whole argument list: `@abi<<here>> var x: Int`
//
// It's difficult to write code that recovers from both #2 and #3. The problem is that in both cases, what comes
// next looks like a declaration, so a simple lookahead cannot distinguish between them--you'd have to parse all
// the way to the closing paren. (And what if *that's* also missing?)
//
// In lieu of that, we judge that recovering gracefully from #3 is more important than #2 and therefore do not even
// attempt to parse the argument unless we've seen a left paren.
guard !missingLParen && !self.at(.rightParen) else {
return makeMissingProviderArguments(unexpectedBefore: [])
}

let decl = parseDeclaration(in: .argumentList)

guard let provider = RawABIAttributeArgumentsSyntax.Provider(decl) else {
return makeMissingProviderArguments(unexpectedBefore: [decl.raw])
}

return RawABIAttributeArgumentsSyntax(provider: provider, arena: self.arena)
}
}

extension Parser {
mutating func parseBackDeployedAttributeArguments() -> RawBackDeployedAttributeArgumentsSyntax {
let (unexpectedBeforeLabel, label) = self.expect(.keyword(.before))
Expand Down
Loading