Skip to content

[Parser] Attributes on MacroExpansionDeclSyntax #1650

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 1 commit into from
May 15, 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
12 changes: 12 additions & 0 deletions CodeGeneration/Sources/SyntaxSupport/DeclNodes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1278,6 +1278,18 @@ public let DECL_NODES: [Node] = [
"FreestandingMacroExpansion"
],
children: [
Child(
name: "Attributes",
kind: .collection(kind: "AttributeList", collectionElementName: "Attribute"),
nameForDiagnostics: "attributes",
isOptional: true
),
Child(
name: "Modifiers",
kind: .collection(kind: "ModifierList", collectionElementName: "Modifier"),
nameForDiagnostics: "modifiers",
isOptional: true
),
Child(
name: "PoundToken",
kind: .token(choices: [.token(tokenKind: "PoundToken")]),
Expand Down
67 changes: 54 additions & 13 deletions Sources/SwiftParser/Declarations.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,18 +29,25 @@ extension DeclarationModifier {
}

extension TokenConsumer {
mutating func atStartOfFreestandingMacroExpansion() -> Bool {
if !self.at(.pound) {
return false
}
if self.peek().rawTokenKind != .identifier && !self.peek().isLexerClassifiedKeyword {
return false
}
if self.currentToken.trailingTriviaByteLength != 0 || self.peek().leadingTriviaByteLength != 0 {
return false
}
return true
}

mutating func atStartOfDeclaration(
isAtTopLevel: Bool = false,
allowInitDecl: Bool = true,
allowRecovery: Bool = false
) -> Bool {
if self.at(anyIn: PoundDeclarationStart.self) != nil {
// Don't treat freestanding macro expansions as declarations. They'll be
// parsed as expressions.
if self.at(.pound) {
return false
}

return true
}

Expand All @@ -53,12 +60,14 @@ extension TokenConsumer {
_ = subparser.consumeAttributeList()
}

var hasModifier = false
if subparser.currentToken.isLexerClassifiedKeyword || subparser.currentToken.rawTokenKind == .identifier {
var modifierProgress = LoopProgressCondition()
while let (modifierKind, handle) = subparser.at(anyIn: DeclarationModifier.self),
modifierKind != .class,
modifierProgress.evaluate(subparser.currentToken)
{
hasModifier = true
subparser.eat(handle)
if modifierKind != .open && subparser.at(.leftParen) && modifierKind.canHaveParenthesizedArgument {
// When determining whether we are at a declaration, don't consume anything in parentheses after 'open'
Expand Down Expand Up @@ -113,6 +122,16 @@ extension TokenConsumer {
case .macroKeyword:
// macro Foo ...
return subparser.peek().rawTokenKind == .identifier
case .pound:
// Force parsing '#<identifier>' after attributes as a macro expansion decl.
if hasAttribute || hasModifier {
return true
}

// Otherwise, parse it as a expression.
// FIXME: C++ parser returns true if this is a top-level non-"script" files.
// But we don't have "is library" flag.
return false
case .some(_):
// All other decl start keywords unconditonally start a decl.
return true
Expand Down Expand Up @@ -203,10 +222,6 @@ extension Parser {
return .decls(RawMemberDeclListSyntax(elements: elements, arena: parser.arena))
}
return RawDeclSyntax(directive)
case (.pound, _)?:
// FIXME: If we can have attributes for macro expansions, handle this
// via DeclarationStart.
return RawDeclSyntax(self.parseMacroExpansionDeclaration())
case nil:
break
}
Expand Down Expand Up @@ -258,6 +273,8 @@ extension Parser {
return RawDeclSyntax(self.parseNominalTypeDeclaration(for: RawActorDeclSyntax.self, attrs: attrs, introucerHandle: handle))
case (.macroKeyword, let handle)?:
return RawDeclSyntax(self.parseMacroDeclaration(attrs: attrs, introducerHandle: handle))
case (.pound, let handle)?:
return RawDeclSyntax(self.parseMacroExpansionDeclaration(attrs, handle))
case nil:
if inMemberDeclList {
let isProbablyVarDecl = self.at(.identifier, .wildcard) && self.peek().rawTokenKind.is(.colon, .equal, .comma)
Expand Down Expand Up @@ -2023,9 +2040,30 @@ extension Parser {
/// =======
///
/// macro-expansion-declaration → '#' identifier expr-call-suffix?
mutating func parseMacroExpansionDeclaration() -> RawMacroExpansionDeclSyntax {
let poundKeyword = self.consumeAnyToken()
let (unexpectedBeforeMacro, macro) = self.expectIdentifier()
mutating func parseMacroExpansionDeclaration(
_ attrs: DeclAttributes,
_ handle: RecoveryConsumptionHandle
) -> RawMacroExpansionDeclSyntax {

let (unexpectedBeforePound, poundKeyword) = self.eat(handle)
// Don't allow space between '#' and the macro name.
if poundKeyword.trailingTriviaByteLength != 0 || self.currentToken.leadingTriviaByteLength != 0 {
return RawMacroExpansionDeclSyntax(
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The intent here is that parse # followed by whitespaces as #<missing identifier>, and let the caller do the recovery.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should still continue macro attribute parsing if the identifier is at the same line as the #. I would expect that to yield better recovery.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What would you expect as the parsed tree. How do we embed "hasError"?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For example if you have # elif, we should have unexpectedBeforeMacroName = ' elif' and macroName should be a missing identifier with text elif. That’s also what we do when parsing a. b (where whitespace around the . is not balanced).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. Will do.

attributes: attrs.attributes,
modifiers: attrs.modifiers,
unexpectedBeforePound,
poundToken: poundKeyword,
macro: self.missingToken(.identifier),
genericArguments: nil,
leftParen: nil,
argumentList: .init(elements: [], arena: self.arena),
rightParen: nil,
trailingClosure: nil,
additionalTrailingClosures: nil,
arena: self.arena
)
}
let (unexpectedBeforeMacro, macro) = self.expectIdentifier(keywordRecovery: true)

// Parse the optional generic argument list.
let generics: RawGenericArgumentClauseSyntax?
Expand Down Expand Up @@ -2063,6 +2101,9 @@ extension Parser {
}

return RawMacroExpansionDeclSyntax(
attributes: attrs.attributes,
modifiers: attrs.modifiers,
unexpectedBeforePound,
poundToken: poundKeyword,
unexpectedBeforeMacro,
macro: macro,
Expand Down
15 changes: 14 additions & 1 deletion Sources/SwiftParser/Expressions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1394,8 +1394,21 @@ extension Parser {
pattern: PatternContext,
flavor: ExprFlavor
) -> RawMacroExpansionExprSyntax {
if !atStartOfFreestandingMacroExpansion() {
return RawMacroExpansionExprSyntax(
poundToken: self.consumeAnyToken(),
macro: self.missingToken(.identifier),
genericArguments: nil,
leftParen: nil,
argumentList: .init(elements: [], arena: self.arena),
rightParen: nil,
trailingClosure: nil,
additionalTrailingClosures: nil,
arena: self.arena
)
}
let poundKeyword = self.consumeAnyToken()
let (unexpectedBeforeMacro, macro) = self.expectIdentifier()
let (unexpectedBeforeMacro, macro) = self.expectIdentifier(keywordRecovery: true)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

e.g. #class is invalid but should be recoverable.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add a test case for #<keyword>?


// Parse the optional generic argument list.
let generics: RawGenericArgumentClauseSyntax?
Expand Down
6 changes: 3 additions & 3 deletions Sources/SwiftParser/TokenSpecSet.swift
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,7 @@ enum DeclarationStart: TokenSpecSet {
case typealiasKeyword
case varKeyword
case inoutKeyword
case pound

init?(lexeme: Lexer.Lexeme) {
switch PrepareForKeywordMatch(lexeme) {
Expand All @@ -295,6 +296,7 @@ enum DeclarationStart: TokenSpecSet {
case TokenSpec(.typealias): self = .typealiasKeyword
case TokenSpec(.var): self = .varKeyword
case TokenSpec(.inout): self = .inoutKeyword
case TokenSpec(.pound): self = .pound
default: return nil
}
}
Expand All @@ -321,6 +323,7 @@ enum DeclarationStart: TokenSpecSet {
case .typealiasKeyword: return .keyword(.typealias)
case .varKeyword: return .keyword(.var)
case .inoutKeyword: return TokenSpec(.inout, recoveryPrecedence: .declKeyword)
case .pound: return TokenSpec(.pound, recoveryPrecedence: .openingPoundIf)
}
}
}
Expand Down Expand Up @@ -396,20 +399,17 @@ enum OperatorLike: TokenSpecSet {

enum PoundDeclarationStart: TokenSpecSet {
case poundIfKeyword
case pound

init?(lexeme: Lexer.Lexeme) {
switch lexeme.rawTokenKind {
case .poundIfKeyword: self = .poundIfKeyword
case .pound: self = .pound
default: return nil
}
}

var spec: TokenSpec {
switch self {
case .poundIfKeyword: return .poundIfKeyword
case .pound: return .pound
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,10 @@ private func childNameForDiagnostics(_ keyPath: AnyKeyPath) -> String? {
return "macro definition"
case \MacroDeclSyntax.genericWhereClause:
return "generic where clause"
case \MacroExpansionDeclSyntax.attributes:
return "attributes"
case \MacroExpansionDeclSyntax.modifiers:
return "modifiers"
case \MemberAccessExprSyntax.base:
return "base"
case \MemberAccessExprSyntax.name:
Expand Down
12 changes: 10 additions & 2 deletions Sources/SwiftSyntax/generated/ChildNameForKeyPath.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1995,8 +1995,16 @@ public func childName(_ keyPath: AnyKeyPath) -> String? {
return "genericWhereClause"
case \MacroDeclSyntax.unexpectedAfterGenericWhereClause:
return "unexpectedAfterGenericWhereClause"
case \MacroExpansionDeclSyntax.unexpectedBeforePoundToken:
return "unexpectedBeforePoundToken"
case \MacroExpansionDeclSyntax.unexpectedBeforeAttributes:
return "unexpectedBeforeAttributes"
case \MacroExpansionDeclSyntax.attributes:
return "attributes"
case \MacroExpansionDeclSyntax.unexpectedBetweenAttributesAndModifiers:
return "unexpectedBetweenAttributesAndModifiers"
case \MacroExpansionDeclSyntax.modifiers:
return "modifiers"
case \MacroExpansionDeclSyntax.unexpectedBetweenModifiersAndPoundToken:
return "unexpectedBetweenModifiersAndPoundToken"
case \MacroExpansionDeclSyntax.poundToken:
return "poundToken"
case \MacroExpansionDeclSyntax.unexpectedBetweenPoundTokenAndMacro:
Expand Down
Loading