Skip to content

Commit 976f163

Browse files
committed
Add experimental @abi attribute to SwiftSyntax
Adds basic support for parsing the `@abi` attribute. This commit doesn’t support everything we eventually want it to—particularly, significant refactoring of the syntax tree will be needed to support nominal types—but it handles enough to match what’s implemented in the compiler so far.
1 parent 1cd3534 commit 976f163

25 files changed

+1193
-23
lines changed

CodeGeneration/Sources/SyntaxSupport/AttributeNodes.swift

+29
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,11 @@ public let ATTRIBUTE_NODES: [Node] = [
139139
name: "documentationArguments",
140140
kind: .node(kind: .documentationAttributeArgumentList)
141141
),
142+
Child(
143+
name: "abiArguments",
144+
kind: .node(kind: .abiAttributeArguments),
145+
experimentalFeature: .abiAttribute
146+
),
142147
]),
143148
documentation: """
144149
The arguments of the attribute.
@@ -267,6 +272,30 @@ public let ATTRIBUTE_NODES: [Node] = [
267272
]
268273
),
269274

275+
Node(
276+
kind: .abiAttributeArguments,
277+
base: .syntax,
278+
experimentalFeature: .abiAttribute,
279+
nameForDiagnostics: "ABI-providing declaration",
280+
documentation: "The arguments of the '@abi' attribute",
281+
children: [
282+
Child(
283+
name: "provider",
284+
kind: .nodeChoices(choices: [
285+
Child(name: "associatedType", kind: .node(kind: .associatedTypeDecl)),
286+
Child(name: "deinitializer", kind: .node(kind: .deinitializerDecl)),
287+
Child(name: "enumCase", kind: .node(kind: .enumCaseDecl)),
288+
Child(name: "function", kind: .node(kind: .functionDecl)),
289+
Child(name: "initializer", kind: .node(kind: .initializerDecl)),
290+
Child(name: "missing", kind: .node(kind: .missingDecl)),
291+
Child(name: "subscript", kind: .node(kind: .subscriptDecl)),
292+
Child(name: "typeAlias", kind: .node(kind: .typeAliasDecl)),
293+
Child(name: "variable", kind: .node(kind: .variableDecl)),
294+
])
295+
)
296+
]
297+
),
298+
270299
Node(
271300
kind: .conventionAttributeArguments,
272301
base: .syntax,

CodeGeneration/Sources/SyntaxSupport/ExperimentalFeatures.swift

+5
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ public enum ExperimentalFeature: String, CaseIterable {
2020
case trailingComma
2121
case coroutineAccessors
2222
case valueGenerics
23+
case abiAttribute
2324

2425
/// The name of the feature as it is written in the compiler's `Features.def` file.
2526
public var featureName: String {
@@ -38,6 +39,8 @@ public enum ExperimentalFeature: String, CaseIterable {
3839
return "CoroutineAccessors"
3940
case .valueGenerics:
4041
return "ValueGenerics"
42+
case .abiAttribute:
43+
return "ABIAttribute"
4144
}
4245
}
4346

@@ -58,6 +61,8 @@ public enum ExperimentalFeature: String, CaseIterable {
5861
return "coroutine accessors"
5962
case .valueGenerics:
6063
return "value generics"
64+
case .abiAttribute:
65+
return "@abi attribute"
6166
}
6267
}
6368

CodeGeneration/Sources/SyntaxSupport/KeywordSpec.swift

+3
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ public enum Keyword: CaseIterable {
131131
case _underlyingVersion
132132
case _UnknownLayout
133133
case _version
134+
case abi
134135
case accesses
135136
case actor
136137
case addressWithNativeOwner
@@ -405,6 +406,8 @@ public enum Keyword: CaseIterable {
405406
return KeywordSpec("_UnknownLayout")
406407
case ._version:
407408
return KeywordSpec("_version")
409+
case .abi:
410+
return KeywordSpec("abi", experimentalFeature: .abiAttribute)
408411
case .accesses:
409412
return KeywordSpec("accesses")
410413
case .actor:

CodeGeneration/Sources/SyntaxSupport/SyntaxNodeKind.swift

+11-1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ public enum SyntaxNodeKind: String, CaseIterable, IdentifierConvertible, TypeCon
2222

2323
case _canImportExpr
2424
case _canImportVersionInfo
25+
case abiAttributeArguments
2526
case accessorBlock
2627
case accessorDecl
2728
case accessorDeclList
@@ -340,14 +341,23 @@ public enum SyntaxNodeKind: String, CaseIterable, IdentifierConvertible, TypeCon
340341
return .identifier(rawValue)
341342
}
342343

344+
public var uppercasedFirstWordRawValue: String {
345+
switch self {
346+
case .abiAttributeArguments:
347+
"ABIAttributeArguments"
348+
default:
349+
rawValue.withFirstCharacterUppercased
350+
}
351+
}
352+
343353
public var syntaxType: TypeSyntax {
344354
switch self {
345355
case .syntax:
346356
return "Syntax"
347357
case .syntaxCollection:
348358
return "SyntaxCollection"
349359
default:
350-
return "\(raw: rawValue.withFirstCharacterUppercased)Syntax"
360+
return "\(raw: uppercasedFirstWordRawValue)Syntax"
351361
}
352362
}
353363

CodeGeneration/Sources/Utils/SyntaxBuildableType.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ public struct SyntaxBuildableType: Hashable {
151151
public var resultBuilderType: TypeSyntax {
152152
switch kind {
153153
case .node(kind: let kind):
154-
return TypeSyntax("\(raw: kind.rawValue.withFirstCharacterUppercased)Builder")
154+
return TypeSyntax("\(raw: kind.uppercasedFirstWordRawValue)Builder")
155155
case .token:
156156
preconditionFailure("Tokens cannot be constructed using result builders")
157157
}

CodeGeneration/Sources/generate-swift-syntax/templates/swiftsyntax/SyntaxEnumFile.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ let syntaxEnumFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
6060

6161
for base in SYNTAX_NODES where base.kind.isBase {
6262
let baseKind = base.kind
63-
let baseName = baseKind.rawValue.withFirstCharacterUppercased
63+
let baseName = baseKind.uppercasedFirstWordRawValue
6464
let enumType: TypeSyntax = "\(raw: baseName)SyntaxEnum"
6565

6666
try! EnumDeclSyntax(

Sources/SwiftParser/Attributes.swift

+71-4
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@
1111
//===----------------------------------------------------------------------===//
1212

1313
#if swift(>=6)
14-
@_spi(RawSyntax) internal import SwiftSyntax
14+
@_spi(ExperimentalLanguageFeatures) @_spi(RawSyntax) internal import SwiftSyntax
1515
#else
16-
@_spi(RawSyntax) import SwiftSyntax
16+
@_spi(ExperimentalLanguageFeatures) @_spi(RawSyntax) import SwiftSyntax
1717
#endif
1818

1919
extension Parser {
@@ -58,6 +58,7 @@ extension Parser {
5858
case _typeEraser
5959
case _unavailableFromAsync
6060
case `rethrows`
61+
case abi
6162
case attached
6263
case available
6364
case backDeployed
@@ -95,6 +96,7 @@ extension Parser {
9596
case TokenSpec(._typeEraser): self = ._typeEraser
9697
case TokenSpec(._unavailableFromAsync): self = ._unavailableFromAsync
9798
case TokenSpec(.`rethrows`): self = .rethrows
99+
case TokenSpec(.abi) where experimentalFeatures.contains(.abiAttribute): self = .abi
98100
case TokenSpec(.attached): self = .attached
99101
case TokenSpec(.available): self = .available
100102
case TokenSpec(.backDeployed): self = .backDeployed
@@ -136,6 +138,7 @@ extension Parser {
136138
case ._typeEraser: return .keyword(._typeEraser)
137139
case ._unavailableFromAsync: return .keyword(._unavailableFromAsync)
138140
case .`rethrows`: return .keyword(.rethrows)
141+
case .abi: return .keyword(.abi)
139142
case .attached: return .keyword(.attached)
140143
case .available: return .keyword(.available)
141144
case .backDeployed: return .keyword(.backDeployed)
@@ -176,9 +179,16 @@ extension Parser {
176179
case noArgument
177180
}
178181

182+
/// Parse the argument of an attribute, if it has one.
183+
///
184+
/// - Parameters:
185+
/// - argumentMode: Indicates whether the attribute must, may, or may not have an argument.
186+
/// - parseArguments: Called to parse the argument list. If there is an opening parenthesis, it will have already been consumed.
187+
/// - parseMissingArguments: If provided, called instead of `parseArgument` when an argument list was required but no opening parenthesis was present.
179188
mutating func parseAttribute(
180189
argumentMode: AttributeArgumentMode,
181-
parseArguments: (inout Parser) -> RawAttributeSyntax.Arguments
190+
parseArguments: (inout Parser) -> RawAttributeSyntax.Arguments,
191+
parseMissingArguments: ((inout Parser) -> RawAttributeSyntax.Arguments)? = nil
182192
) -> RawAttributeListSyntax.Element {
183193
var (unexpectedBeforeAtSign, atSign) = self.expect(.atSign)
184194
if atSign.trailingTriviaByteLength > 0 || self.currentToken.leadingTriviaByteLength > 0 {
@@ -213,7 +223,12 @@ extension Parser {
213223
)
214224
leftParen = leftParen.tokenView.withTokenDiagnostic(tokenDiagnostic: diagnostic, arena: self.arena)
215225
}
216-
let argument = parseArguments(&self)
226+
let argument: RawAttributeSyntax.Arguments
227+
if let parseMissingArguments, leftParen.presence == .missing {
228+
argument = parseMissingArguments(&self)
229+
} else {
230+
argument = parseArguments(&self)
231+
}
217232
let (unexpectedBeforeRightParen, rightParen) = self.expect(.rightParen)
218233
return .attribute(
219234
RawAttributeSyntax(
@@ -255,6 +270,12 @@ extension Parser {
255270
}
256271

257272
switch peek(isAtAnyIn: DeclarationAttributeWithSpecialSyntax.self) {
273+
case .abi:
274+
return parseAttribute(argumentMode: .required) { parser in
275+
return .abiArguments(parser.parseABIAttributeArguments())
276+
} parseMissingArguments: { parser in
277+
return .abiArguments(parser.parseABIAttributeArguments(missingLParen: true))
278+
}
258279
case .available, ._spi_available:
259280
return parseAttribute(argumentMode: .required) { parser in
260281
return .availability(parser.parseAvailabilityArgumentSpecList())
@@ -918,6 +939,52 @@ extension Parser {
918939
}
919940
}
920941

942+
extension Parser {
943+
/// Parse the arguments inside an `@abi(...)` attribute.
944+
///
945+
/// - Parameter missingLParen: `true` if the opening paren for the argument list was missing.
946+
mutating func parseABIAttributeArguments(missingLParen: Bool = false) -> RawABIAttributeArgumentsSyntax {
947+
func makeMissingProviderArguments(unexpectedBefore: [RawSyntax]) -> RawABIAttributeArgumentsSyntax {
948+
return RawABIAttributeArgumentsSyntax(
949+
provider: .missing(
950+
RawMissingDeclSyntax(
951+
unexpectedBefore.isEmpty ? nil : RawUnexpectedNodesSyntax(elements: unexpectedBefore, arena: self.arena),
952+
attributes: self.emptyCollection(RawAttributeListSyntax.self),
953+
modifiers: self.emptyCollection(RawDeclModifierListSyntax.self),
954+
placeholder: self.missingToken(.identifier, text: "<#declaration#>"),
955+
arena: arena
956+
)
957+
),
958+
arena: self.arena
959+
)
960+
}
961+
962+
// Consider the three kinds of mistakes we might see here:
963+
//
964+
// 1. The user forgot the argument: `@abi(<<here>>) var x: Int`
965+
// 2. The user forgot the left paren: `@abi<<here>> var x_abi: Int) var x: Int`
966+
// 3. The user forgot the whole argument list: `@abi<<here>> var x: Int`
967+
//
968+
// It's difficult to write code that recovers from both #2 and #3. The problem is that in both cases, what comes
969+
// next looks like a declaration, so a simple lookahead cannot distinguish between them--you'd have to parse all
970+
// the way to the closing paren. (And what if *that's* also missing?)
971+
//
972+
// In lieu of that, we judge that recovering gracefully from #3 is more important than #2 and therefore do not even
973+
// attempt to parse the argument unless we've seen a left paren.
974+
guard !missingLParen && !self.at(.rightParen) else {
975+
return makeMissingProviderArguments(unexpectedBefore: [])
976+
}
977+
978+
let decl = parseDeclaration(in: .argumentList)
979+
980+
guard let provider = RawABIAttributeArgumentsSyntax.Provider(decl) else {
981+
return makeMissingProviderArguments(unexpectedBefore: [decl.raw])
982+
}
983+
984+
return RawABIAttributeArgumentsSyntax(provider: provider, arena: self.arena)
985+
}
986+
}
987+
921988
extension Parser {
922989
mutating func parseBackDeployedAttributeArguments() -> RawBackDeployedAttributeArgumentsSyntax {
923990
let (unexpectedBeforeLabel, label) = self.expect(.keyword(.before))

Sources/SwiftParser/Declarations.swift

+52-11
Original file line numberDiff line numberDiff line change
@@ -168,11 +168,50 @@ extension Parser {
168168
}
169169
}
170170

171+
/// Describes the context around a declaration in order to modify how it is parsed.
172+
enum DeclarationParseContext {
173+
/// The declaration is in top-level code or a function body; that is, it may be mixed with statements and
174+
/// expressions.
175+
case topLevelOrCodeBlock
176+
177+
/// The declaration is in a member list.
178+
case memberDeclList
179+
180+
/// The declaration is in an argument list (for instance, of an `@abi` attribute).
181+
case argumentList
182+
183+
/// Should the parser assume that any syntax it encounters here *must* be declaration syntax? This allows more
184+
/// aggressive recovery which might misinterpret statement or expression syntax as malformed declaration syntax.
185+
var requiresDecl: Bool {
186+
switch self {
187+
case .topLevelOrCodeBlock:
188+
return false
189+
case .memberDeclList, .argumentList:
190+
return true
191+
}
192+
}
193+
194+
/// If an introducer is not found at the expected location, what token should terminate our search for one?
195+
var recoveryPrecedence: TokenPrecedence? {
196+
switch self {
197+
case .topLevelOrCodeBlock:
198+
// Scan as far as we want.
199+
return nil
200+
case .memberDeclList:
201+
// Don't scan past the enclosing brace.
202+
return .closingBrace
203+
case .argumentList:
204+
// Don't scan past the closing parenthesis.
205+
return .weakBracketed(closingDelimiter: .rightParen)
206+
}
207+
}
208+
}
209+
171210
/// Parse a declaration.
172211
///
173-
/// If `inMemberDeclList` is `true`, we know that the next item must be a
174-
/// declaration and thus start with a keyword. This allows further recovery.
175-
mutating func parseDeclaration(inMemberDeclList: Bool = false) -> RawDeclSyntax {
212+
/// - Parameter context: Describes the code around the declaration being parsed. This affects how the parser tries
213+
/// to recover from malformed syntax in the declaration.
214+
mutating func parseDeclaration(in context: DeclarationParseContext = .topLevelOrCodeBlock) -> RawDeclSyntax {
176215
// If we are at a `#if` of attributes, the `#if` directive should be
177216
// parsed when we're parsing the attributes.
178217
if self.at(.poundIf) && !self.withLookahead({ $0.consumeIfConfigOfAttributes() }) {
@@ -221,8 +260,10 @@ extension Parser {
221260
// to parse.
222261
// If we are inside a memberDecl list, we don't want to eat closing braces (which most likely close the outer context)
223262
// while recovering to the declaration start.
224-
let recoveryPrecedence = inMemberDeclList ? TokenPrecedence.closingBrace : nil
225-
recoveryResult = self.canRecoverTo(anyIn: DeclarationKeyword.self, overrideRecoveryPrecedence: recoveryPrecedence)
263+
recoveryResult = self.canRecoverTo(
264+
anyIn: DeclarationKeyword.self,
265+
overrideRecoveryPrecedence: context.recoveryPrecedence
266+
)
226267
}
227268

228269
switch recoveryResult {
@@ -273,17 +314,17 @@ extension Parser {
273314
case (.lhs(.pound), let handle)?:
274315
return RawDeclSyntax(self.parseMacroExpansionDeclaration(attrs, handle))
275316
case (.rhs, let handle)?:
276-
return RawDeclSyntax(self.parseBindingDeclaration(attrs, handle, inMemberDeclList: inMemberDeclList))
317+
return RawDeclSyntax(self.parseBindingDeclaration(attrs, handle, in: context))
277318
case nil:
278319
break
279320
}
280321

281-
if inMemberDeclList {
322+
if context.requiresDecl {
282323
let isProbablyVarDecl = self.at(.identifier, .wildcard) && self.peek(isAt: .colon, .equal, .comma)
283324
let isProbablyTupleDecl = self.at(.leftParen) && self.peek(isAt: .identifier, .wildcard)
284325

285326
if isProbablyVarDecl || isProbablyTupleDecl {
286-
return RawDeclSyntax(self.parseBindingDeclaration(attrs, .missing(.keyword(.var))))
327+
return RawDeclSyntax(self.parseBindingDeclaration(attrs, .missing(.keyword(.var)), in: context))
287328
}
288329

289330
if self.currentToken.isEditorPlaceholder {
@@ -787,7 +828,7 @@ extension Parser {
787828
if self.at(.poundSourceLocation) {
788829
decl = RawDeclSyntax(self.parsePoundSourceLocationDirective())
789830
} else {
790-
decl = self.parseDeclaration(inMemberDeclList: true)
831+
decl = self.parseDeclaration(in: .memberDeclList)
791832
}
792833

793834
let semi = self.consume(if: .semicolon)
@@ -1309,7 +1350,7 @@ extension Parser {
13091350
mutating func parseBindingDeclaration(
13101351
_ attrs: DeclAttributes,
13111352
_ handle: RecoveryConsumptionHandle,
1312-
inMemberDeclList: Bool = false
1353+
in context: DeclarationParseContext
13131354
) -> RawVariableDeclSyntax {
13141355
let (unexpectedBeforeIntroducer, introducer) = self.eat(handle)
13151356
let hasTryBeforeIntroducer = unexpectedBeforeIntroducer?.containsToken(where: { TokenSpec(.try) ~= $0 }) ?? false
@@ -1399,7 +1440,7 @@ extension Parser {
13991440
if (self.at(.leftBrace)
14001441
&& (initializer == nil || !self.currentToken.isAtStartOfLine
14011442
|| self.withLookahead({ $0.atStartOfGetSetAccessor() })))
1402-
|| (inMemberDeclList && self.at(anyIn: AccessorDeclSyntax.AccessorSpecifierOptions.self) != nil
1443+
|| (context.requiresDecl && self.at(anyIn: AccessorDeclSyntax.AccessorSpecifierOptions.self) != nil
14031444
&& !self.at(.keyword(.`init`)))
14041445
{
14051446
accessors = self.parseAccessorBlock()

Sources/SwiftParser/TokenPrecedence.swift

+1
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,7 @@ enum TokenPrecedence: Comparable {
283283
._swift_native_objc_runtime_base,
284284
._typeEraser,
285285
._unavailableFromAsync,
286+
.abi,
286287
.attached,
287288
.available,
288289
.backDeployed,

0 commit comments

Comments
 (0)