Skip to content

Commit

Permalink
Merge pull request #2902 from beccadax/abi-changed-your-first-name
Browse files Browse the repository at this point in the history
Partial SwiftSyntax support for experimental `@abi` attribute
  • Loading branch information
beccadax authored Dec 4, 2024
2 parents 6562544 + 1739cb3 commit 0bf5b6c
Show file tree
Hide file tree
Showing 26 changed files with 1,451 additions and 23 deletions.
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

0 comments on commit 0bf5b6c

Please sign in to comment.