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

SwiftSyntax support for experimental @abi attribute #2882

Open
wants to merge 7 commits into
base: main
Choose a base branch
from

Conversation

beccadax
Copy link
Contributor

@beccadax beccadax commented Oct 16, 2024

Matches swiftlang/swift#76878.

Note that @abi introduces a situation we've never had before: the various DeclGroupSyntax decls can now have a nil memberBlock. This can only occur for children of an ABIAttributeArgumentsSyntax node, not any nodes that would occur in existing constructs. To preserve source compatibility with existing clients, I've made memberBlock an implicitly-unwrapped optional, but I'm open to opinions if that's not the design we'd like to have.

Another place I'd like feedback on is the way I'm parsing malformed @abi argument lists. Because the argument is literally an entire decl, I found that the default recovery behavior when the left paren was missing didn't work well, so I built something a little different. If you have better suggestions, I'd love to hear them.

Edit: This has been changed from the original implementation. The new version extracts a new "header" child node from nodes like DeclClassSyntax which contains everything but the member block. All of the DeclGroup nodes have this change applied. As proof that this works, I've updated the HasTrailingMemberDeclBlock initializer to take a header node, rather than a syntax string.

Generating the new decls I wanted—particularly their compatibility layers—took substantial refactoring of CodeGenerator, including the introduction of a new childHistory mechanism to replace deprecatedName and its ilk, extending it with the capability to model this kind of "extraction" of a child node from a parent. I've also made it so that base nodes can have children (these become protocol requirements), traits can optionally declare a memberwise failable initializer (this must be manually implemented at the moment), and deprecated properties are automatically generated for traits.

@beccadax
Copy link
Contributor Author

With swiftlang/swift#76878

@swift-ci please test

@ahoppen
Copy link
Member

ahoppen commented Oct 16, 2024

I really dislike using implicitly unwrapped optionals here. It’s just waiting to haunt us later when we forget to check for nil. As you already noted, changing memberBlock to be optional has a pretty big API impact (because memberBlock is pretty widely used) and also feels a little weird if the only case where it can actually be nil is inside the relatively low-frequency @abi attribute. Thus, I’d prefer to not make memberBlock optional.

This leaves us with one more option: Add new syntax nodes for all the type declarations. Maybe it would be OK if they are completely separate from the existing type nodes but just shared the parsing logic. I’ll need to think about that some more.

@allevato
Copy link
Member

Is there a way that we could use a non-optional MemberBlockSyntax with an empty item list, and some kind of dummy/missing-but-not-nil leftBrace and rightBrace tokens?

@beccadax
Copy link
Contributor Author

Another option I considered was adding a new MemberBlockSyntax? property to replace memberBlock, and making the backwards-compatibility memberBlock create a dummy missing MemberBlockSyntax when the new property is nil. I'm not sure what we'd call the new property, though. members has already been expended.

@beccadax
Copy link
Contributor Author

beccadax commented Oct 16, 2024

Add new syntax nodes for all the type declarations. Maybe it would be OK if they are completely separate from the existing type nodes but just shared the parsing logic. I’ll need to think about that some more.

I guess this would look something like extracting ClassDeclHeaderSyntax out of ClassDeclSyntax so that you end up with:

// `class SomeClass {}`
- SourceFileSyntax
├─statements: CodeBlockItemListSyntax
│ ├─[0]: CodeBlockItemSyntax
│ │ ╰─item: ClassDeclSyntax
│ │   ├─header: ClassDeclHeaderSyntax
│ │   │ ├─attributes: AttributeListSyntax
│ │   │ ├─modifiers: DeclModifierListSyntax
│ │   │ ├─classKeyword: keyword(_CompilerSwiftSyntax.Keyword.class)
│ │   │ ╰─name: identifier("SomeClass")
│ │   ╰─memberBlock: MemberBlockSyntax
│ │     ├─leftBrace: leftBrace
│ │     ├─members: MemberBlockItemListSyntax
│ │     ╰─rightBrace: rightBrace
...

Then you could put compatibility properties on ClassDeclSyntax that forward to header's corresponding properties. Repeat for each DeclGroupSyntax (or maybe each DeclSyntax period?), and then have ABIAttributeArguments use a child of type DeclHeaderSyntax instead of DeclSyntax.

This is a rather large refactor, but I think it could work. As a bonus, HasTrailingCodeBlock.init(header:bodyBuilder:) would have actual syntax nodes for its headers that it could work with.

@beccadax beccadax force-pushed the abi-changed-your-name branch from de2dc9f to f7dfc81 Compare October 28, 2024 23:18
@beccadax
Copy link
Contributor Author

This has been changed from the original implementation. The new version extracts a new "header" child node from nodes like DeclClassSyntax which contains everything but the member block. All of the DeclGroup nodes have this change applied. As proof that this works, I've updated the HasTrailingMemberDeclBlock initializer to take a header node, rather than a syntax string.

Generating the new decls I wanted—particularly their compatibility layers—took substantial refactoring of CodeGenerator, including the introduction of a new childHistory mechanism to replace deprecatedName and its ilk, extending it with the capability to model this kind of "extraction" of a child node from a parent. I've also made it so that base nodes can have children (these become protocol requirements), traits can optionally declare a memberwise failable initializer (this must be manually implemented at the moment), and deprecated properties are automatically generated for traits.

@beccadax
Copy link
Contributor Author

@ahoppen I'd love your opinion on this modification.

@beccadax
Copy link
Contributor Author

With swiftlang/swift#76878

@swift-ci please test

Copy link
Member

@ahoppen ahoppen left a comment

Choose a reason for hiding this comment

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

Two preliminary comments from an initial look at the changes. I think we’re on the right track but I don’t think it makes sense to give this PR a full review until they are addressed.

@@ -180,13 +180,98 @@ public let COMMON_NODES: [Node] = [
parserFunction: "parseDeclaration"
),

Node(
kind: .declGroupHeader,
Copy link
Member

Choose a reason for hiding this comment

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

None of the other base nodes (DeclSyntax, ExprSyntax etc.) have any children and I think we will get crashes if any of the nodes whose base is .declGroupHeader are layout-incompatible with declGroupHeader. DeclGroupHeaderSyntax will assume that the first child in the node is attributes but that might not be the case for a node inheriting from .declGroupHeader.

That said, I would make the base of all header nodes .syntax and create a trait for the decl group headers instead.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm a little reluctant to do this because I get a fair bit of mileage out of having DeclGroupHeaderSyntax be a concrete type instead of an existential. For instance, both ABIAttributeSyntax.Provider and the modified version of HasTrailingMemberBlock use the concrete DeclGroupHeaderSyntax; the former could not easily use an existential because there's no way to constrain a child node to have a trait, and the latter because a concrete type is needed to provide a contextual type for the string literal.

(If it helps, the DeclGroupHeaderSyntax accessors don't directly access the children—they indirect through the existential so they use the leaf type's layout.)

If you can suggest another design that would still give me a concrete node which could contain any of the headers, I'd be open to that change.

Copy link
Member

Choose a reason for hiding this comment

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

Let me think about it some more. It might also become clearer to me once #2888 is merged and this PR is rebased so it doesn’t contain those changes.

Copy link
Contributor

Choose a reason for hiding this comment

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

Hey @ahoppen , we now have the other PR merged. Do you have more specific suggestions in mind?

CodeGeneration/Sources/SyntaxSupport/Child.swift Outdated Show resolved Hide resolved
This is accurate for all NamedDeclSyntax conformers so far, so I feel comfortable doing it.

This also improves the accuracy of the “conforms if it can” validation by also checking the trait’s base kind against the node. That eliminates several false positives in the test.
This is the base node for the headers we are about to extract.

Unlike other base nodes, DeclGroupHeaderSyntax has children, which are treated as protocol requirements for derived nodes.
It actually ought to be the union of three token kind subsets; turn it into a custom type so we can do that.
This also involves changing Child.Refactoring to support a new kind of change: “extracted”, in which a group of adjacent children are moved into a new child node.

Future commits will introduce clients that use the new *DeclHeaderSyntax nodes directly.
Clears up a bunch of warnings.
`DeclGroup` now requires an initializer that can build a group from a header and member block. With that in place, we can reimplement `HasTrailingMemberDeclBlock` to avoid reparsing anything. Which is kind of neat.
@beccadax beccadax force-pushed the abi-changed-your-name branch from f7dfc81 to 54ce286 Compare November 5, 2024 17:56
Copy link
Member

@ahoppen ahoppen left a comment

Choose a reason for hiding this comment

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

Left a few comments inline.

Overall, I’m still skeptical of the new DeclGroupHeaderSyntax header base type because:

I would prefer if ABIAttributeSyntax.Provider listed all the headers nodes it supports, just like it already lists types like typealiasDecl and funcDecl.

@@ -267,6 +272,31 @@ public let ATTRIBUTE_NODES: [Node] = [
]
),

Node(
Copy link
Member

Choose a reason for hiding this comment

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

Could you add an entry to the release notes for the new/updated nodes?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

At this point, the node is hidden behind an experimental feature. Should I still document it now?

Copy link
Member

Choose a reason for hiding this comment

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

I probably didn’t put the comment at the right place. I meant adding a release note entry for the nodes that change and that are not SPI, eg. the introduction of ClassDeclSyntax.header.

}

@available(*, deprecated, renamed: "actorHeader.name")
public var name: TokenSyntax {
Copy link
Member

Choose a reason for hiding this comment

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

I’m a little hesitant to deprecate these accessors and am debating whether we should keep them as non-deprecated convenience accessors. Some thoughts on why:

  • Using classDeclSyntax.classHeader.name feels more complicated than just classDeclSyntax.name if you just want to get a class’s name, which shouldn’t be a complicated operation.
    • But then, some other parts of the syntax tree are a little convoluted to access already (eg. GenericArgumentListSyntax contains GenericArgumentSyntax, which contains TypeSyntax and a trailing comma), so not sure if this is really a super strong argument.
  • I expect quite a few codebases to use these properties, so I expect the to incur deprecation warnings in quite a few codebases and they can’t fully switch over to classHeader.name if they need to support older swift-syntax versions as well.
  • The conformance of ClassDeclSyntax to NamedDeclHeader, WithAttributesSyntax, WithGenericParameters, and WithModifiers relies on these accessors. If we deprecate these accessors, we should also deprecate the conformance of ClassDeclSyntax to these traits. But there’s no way to deprecate protocol conformances and I assume that these are used somewhat wildly, so I’m not sure how to do this transition.

@rintaro I would appreciate your opinion here as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I do understand your concerns here; I separated out the deprecation changes into their own commit specifically so you could evaluate this impact.

I could add something in CodeGenerator to allow certain compatibility layer APIs to be non-deprecated. Just let me know what you'd like me to do here.

Copy link
Member

Choose a reason for hiding this comment

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

@rintaro Just chatted and agree that we shouldn’t deprecate ClassDeclSyntax.name.

],
children: [
Child(
name: "actorHeader",
Copy link
Member

Choose a reason for hiding this comment

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

I would just name this header. You’re on an actor declaration, so it’s clear that the header is for an actor.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If there is anything in DeclGroupSyntax that allows access to the header, it needs to have a different name from the header children in all of the concrete nodes since it will need to have a broader type.

Copy link
Member

@ahoppen ahoppen Nov 7, 2024

Choose a reason for hiding this comment

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

I realized that as well as I continued reviewing and think this is an argument for not having a DeclHeaderSyntax node.

Copy link
Contributor Author

@beccadax beccadax Nov 8, 2024

Choose a reason for hiding this comment

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

An alternative I've been considering is making the DeclGroupHeader node non-base and having it be something like:

  Node(
    kind: .declGroupHeader,
    base: .syntax,
    nameForDiagnostics: "declaration group header",
    parserFunction: "parseDeclarationGroupHeader",
    traits: [
      "WithAttributes",
      "WithModifiers",
      "DeclGroupHeaderProtocol", // slight naming issue I'll have to resolve
    ],
    children: [
      Child(
        name: "header",
        kind: .nodeChoices(choices: [
          Child(name: "actorHeader", kind: .node(kind: .actorDeclHeader)),
          Child(name: "classHeader", kind: .node(kind: .classDeclHeader)),
          Child(name: "enumHeader", kind: .node(kind: .enumDeclHeader)),
          Child(name: "extensionHeader", kind: .node(kind: .extensionDeclHeader)),
          Child(name: "protocolHeader", kind: .node(kind: .protocolDeclHeader)),
          Child(name: "structHeader", kind: .node(kind: .structDeclHeader)),
        ])
      )
    ]
  )

I would then add modifiers, attributes, etc. using a handwritten extension switching over the choices, so there'd be no existential overhead. Not quite as elegant, but it still gives us a concrete type that can represent any of the headers.

Copy link
Member

Choose a reason for hiding this comment

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

Do we really need a DeclGroupHeader node at all? Correct me if I’m wrong, but I think the primary use for it is to be an option to ABIAttributeArgumentsSyntax, which already lists all sorts of other declarations. If we listed all the nodes that are subtypes of DeclGroupHeaderSyntax in there as well, I don’t think there’s real need for DeclGroupHeaderSyntax anymore. Or am I missing something?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I also use it in the new implementation for HasTrailingMemberDeclBlock, and I believe I use it in ASTGen to generalize some of the lowering of DeclGroups, but those probably aren't mandatory—they're just nice little tricks I was able to do now that the new nodes existed.

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 none of those are sufficient reason to introduce DeclGroupHeaderSyntax.

Comment on lines +374 to +375
case .declGroupHeader:
return "DeclHeaderSyntax"
Copy link
Member

Choose a reason for hiding this comment

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

Should we rename declGroupHeader to declHeader so this isn’t needed?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm using "DeclGroupHeader" because only the DeclGroup nodes have separate headers. Do you think I'm being too picky with that?

Copy link
Member

Choose a reason for hiding this comment

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

I just find it add that we have this mismatch here. But depending on how we decide on the existence of DeclHeaderSyntax, this may be a moot point anyway.

@@ -63,19 +65,25 @@ extension InitSignature {
)
}

func transformParam(_ param: FunctionParameterSyntax) -> FunctionParameterSyntax {
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
func transformParam(_ param: FunctionParameterSyntax) -> FunctionParameterSyntax {
func transform(parameter: FunctionParameterSyntax) -> FunctionParameterSyntax {

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That feels at odds with how we normally use argument labels because the word parameter is redundant information given the type. Should it just be transform(_:)?

Copy link
Member

Choose a reason for hiding this comment

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

transform(_:) seems too ambiguous to me. If you don’t like transform(parameter:), I don’t have super strong opinions on it and we can keep transformParam(_:)

allowsMemberBlock: Bool
) -> (RawDeclGroupHeaderSyntax, shouldContinueParsing: Bool) {
func eraseToRawDeclGroupHeaderSyntax(
_ result: (some RawDeclGroupHeaderSyntaxNodeProtocol, Bool)
Copy link
Member

Choose a reason for hiding this comment

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

Could you add labels to the tuple elements here? It’s not really obvious what the Bool represents here.

) -> (RawDeclGroupHeaderSyntax, shouldContinueParsing: Bool) {
func eraseToRawDeclGroupHeaderSyntax(
_ result: (some RawDeclGroupHeaderSyntaxNodeProtocol, Bool)
) -> (RawDeclGroupHeaderSyntax, shouldContinueParsing: Bool) {
Copy link
Member

Choose a reason for hiding this comment

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

Same here, I would prefer to also have a label for the RawDeclGroupHeaderSyntax.

}
}

fatalError("Node \(self) does not have a known subtype")
Copy link
Member

Choose a reason for hiding this comment

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

preconditionFailure is mildly preferred over fatalError.

Comment on lines +652 to +655
mutating func parseDeclarationGroup<T>(
for header: T,
shouldParseMemberBlock: Bool = true
) -> T.Declaration where T: DeclarationGroupHeaderTrait {
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
mutating func parseDeclarationGroup<T>(
for header: T,
shouldParseMemberBlock: Bool = true
) -> T.Declaration where T: DeclarationGroupHeaderTrait {
mutating func parseDeclarationGroup<T: DeclarationGroupHeaderTrait>(
for header: T,
shouldParseMemberBlock: Bool = true
) -> T.Declaration {

Comment on lines +657 to +658
if shouldParseMemberBlock {
self.parseMemberBlock(introducer: header.introducer)
Copy link
Member

Choose a reason for hiding this comment

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

Same here, we can’t use if expressions yet.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants