-
Notifications
You must be signed in to change notification settings - Fork 440
Add Buildable gyb #268
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
Add Buildable gyb #268
Conversation
Disclaimer up front: I had a fairly good idea how the tokens should look like, my ideas further down become a lot more vague – there might be an altogether better design. Don’t feel like you have to stick to what I tell you as you dive into the implementation. With that said, the way I would approach it, would be to start of by generating something like the following // Simple syntax element
public struct IntegerLiteral: ExprBuildable {
let digits: Token
public init(digits: Token) {
self.digits = digits
}
public func buildExpr(format: Format, leadingTrivia: Trivia) -> ExprSyntax {
let integerLiteral = SyntaxFactory.makeIntegerLiteralExpr(
digits: SyntaxFactory.makeIntegerLiteral(String(value))
).withLeadingTrivia(leadingTrivia)
return ExprSyntax(integerLiteral)
}
}
// More complex element
public struct ImportDecl: DeclBuildable {
let attributes: AttributeListSyntax
let modifiers: ModifeirListSyntax
let importTok: Token
let importKind: Token
let path: AccessPathSyntax
public init(
attributes: AttributeListSyntax,
modifiers: ModifeirListSyntax,
importTok: Token,
importKind: Token?,
path: AccessPathSyntax,
) {
self.attributes = attributes
self.modifiers = modifiers
self.importTok = importTok
self.importKind = importKind
self.path = path
}
public func buildDecl(format: Format, leadingTrivia: Trivia) -> DeclSyntax {
let importDecl = SyntaxFactory.makeImportDecl(
attributes: attributes,
modifiers: modifiers,
importTok: importTok,
importKind: importKind,
path: path
).withLeadingTrivia(leadingTrivia)
return DeclSyntax(importDecl)
}
}
// Syntax collection
public struct CodeBlockItemList: SyntaxBuildable {
let elements: [CodeBlockItem]
public init(elements: [CodeBlockItem]) {
elements = elements
}
public func buildSyntax(format: Format, leadingTrivia: Trivia) -> DeclSyntax {
let codeBlockItemList = makeCodeBlockItemList(elements)
.withLeadingTrivia(leadingTrivia)
return Syntax(codeBlockItemList)
}
Notice how each buildable takes exactly the arguments that are specified, so it’s essentially a wrapper around Once we have that, I imagine we can improve it as I described in the previous long post. |
b8ca9e3
to
939b948
Compare
6ac2cbe
to
b2694e2
Compare
fbc500c
to
df1a28b
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks quite good already. I see it really starting to take shape. I’ve posted a few comments inline.
I think it would also be great to have one or two test cases for the buildables. Not only to test that they actually work, but also to give us a feeling for how the API currently behaves (and how we can improve it in the future 😉). Doesn’t have to be anything fancy. I think just an enum
case, a struct decl or something like this should be enough, just pick whatever is easiest to construct.
e7e7d67
to
d381b2c
Compare
Okay @ahoppen I have pushed my progress now, but tests don't pass yet! I know, but I think I need some help 😅 I have tried to make what you mentioned in this comment. I found out that the API with If we take the SourceFileTests ␣import SwiftSyntax
struct ExampleStruct {
let syntax: Syntax
} But get ␣import ␣SwiftSyntax␣struct ExampleStruct ␣{
␣let ␣syntax␣: ␣Syntax} I have also tried to add some of the convince extensions for The Python code is still some messy for me. Thanks! |
Trivia and indentation
Really good indentation will probably be hard to get to work in any case, so for now I’d settle with indentation that compiles, is not too horrible to look at and which e.g. swift-format could improve. But what we have right now is definitely not what we want. What do you think about adding a newline after every line in a code block and after every member declaration? I.e. add a boolean flag to MemberDeclList and CodeBlock like SyntaxFactory.make${node.syntax_kind}(elements.map {
$0.build${node.collection_element}(format: format, leadingTrivia: .newlines(1) + format._makeIndent())
}) (side node: you are currently passing With that, I think, we would get the following result: import␣SwiftSyntax␣
struct␣ExampleStruct␣{
let␣syntax␣:␣Syntax
let␣syntax2␣:␣Syntax} which is already a lot better (and compiles 🎉). Now two things remain:
For 1.) I would suggest to add a property if child.is_indented:
let format = format._indented() For 2.) I would suggest adding a property With 1.) and 2.) in, I think we should get what we want import␣SwiftSyntax␣
struct␣ExampleStruct␣{
␣␣let␣syntax␣:␣Syntax
␣␣let␣syntax2␣:␣Syntax
} A background note on my suggestion: I would implement the entire indentation behavior on the level of the Convenience extensions
I would like to keep any convenience extensions out of this PR for now to (a) make it smaller and easier to review and (b) to see how far we can get with the purely generated code. If this means you need to delete some test cases, that’s totally fine with me. We can always add them again later. Python code
Actually, I think it’s fine. I just skimmed over it and was able to understand what’s going on. Perhaps we can move the code that determines what type a def syntax_buildable_child_type(syntax_kind, is_optional=False):
if 'Token' in syntax_kind: # Alternatively, add a `is_token` method to `Node`
buildable_type = 'TokenSyntax'
elif node.syntax_kind in SYNTAX_BASE_KINDS:
buildable_type = syntax_kind + 'Buildable'
else:
buildable_type = syntax_kind
if is_optional:
buildable_type += '?'
return buildable_type and then use it in line 69 and line 123.
|
07dfa2b
to
2b64f5b
Compare
I have added your suggestions and almost something that generates some code (But will not compile yet). Should we add something like
Or can we make some general rule somewhere for applying all places so we don't need to add the something multiple places?
I have tried to add it to the gyb file? |
I haven’t looked at the code yet. Let me know once you think that is ready for review.
I don’t think that’s necessary. IIUC when we generate the struct-Keyword with struct␣TestStruct␣{
} Please correct me if I’m wrong. Later, when we create convenience constructors that take Strings instead of
Sorry, I don’t quite understand the question. What is “it” referring to? |
11aae73
to
49c9646
Compare
buildable_type = syntax_kind + 'Buildable' | ||
elif not is_token: | ||
buildable_type = syntax_kind | ||
elif 'List' in syntax_kind: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is needed to add correct type here etc.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the original syntax types we are using child.type_name
instead of child.syntax_kind
for the children type here and node.collection_element_type
instead of node.collection_element
for the syntax collection’s element types here.
I think if you use that, you don’t need the special cases for List
and Token
. What do you think about something like the following:
def syntax_buildable_child_type(type_name, syntax_kind, is_optional=False):
if syntax_kind in SYNTAX_BASE_KINDS:
buildable_type = syntax_kind + 'Buildable'
else:
buildable_type = type_name
...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have just tried this, and ended up with something that we wanted with
syntax_buildable_child_type(child.type_name, child.syntax_kind, child.is_token(), child.is_optional)
or
syntax_buildable_child_type(node.collection_element_type, node.collection_element, node.is_token())
Look at the example below:
public struct AvailabilityVersionRestriction: SyntaxBuildable {
let platform: TokenSyntax
let version: VersionTupleSyntax?
public init(
platform: TokenSyntax,
version: VersionTupleSyntax?
) {
self.platform = platform
self.version = version
}
...
}
But we want
public struct AvailabilityVersionRestriction: SyntaxBuildable {
let platform: TokenSyntax
let version: VersionTuple?
public init(
platform: TokenSyntax,
version: VersionTuple?
) {
self.platform = platform
self.version = version
}
...
}
Maybe I'm doing something wrong?
Okay @ahoppen, after some evenings working on it, I have something that works now.
If you have time, it would be nice if you would look a little on the tests I have added.
I figured it out, had some problems where to add the code, that is added here. I first tried to add it add bottom of the gyb file, like this
Again, thanks for your patience 😁 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I’ve added some comments inline but they are all mostly stylistic now, nothing major.
Once you’ve addressed them, I will give the PR another review, run the test suite and get this merged. I’ve lost track but are there any related. If there are any, could you also push any pending changes to your apple/swift PR for that?
The tests look really good to me and I think we are on the right track to some really nice and exhaustive API if you imagine the following changes (which I believe are minor/trivial, but still would like to keep out of this PR)! 🎉
- Allow omitting arguments that are
nil
- Allow omitting arguments that take a specific token and default them to that token
- Allow passing string literals for identifiers
- Create a result builder API for
SyntaxCollections
buildable_type = syntax_kind + 'Buildable' | ||
elif not is_token: | ||
buildable_type = syntax_kind | ||
elif 'List' in syntax_kind: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the original syntax types we are using child.type_name
instead of child.syntax_kind
for the children type here and node.collection_element_type
instead of node.collection_element
for the syntax collection’s element types here.
I think if you use that, you don’t need the special cases for List
and Token
. What do you think about something like the following:
def syntax_buildable_child_type(type_name, syntax_kind, is_optional=False):
if syntax_kind in SYNTAX_BASE_KINDS:
buildable_type = syntax_kind + 'Buildable'
else:
buildable_type = type_name
...
% else: | ||
public protocol ${kind}ListBuildable: SyntaxListBuildable { | ||
% end | ||
func build${kind}List(format: Format, leadingTrivia: Trivia?) -> [${build_kind}] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a reason why Trivia is an Optional? Wouldn’t it be easier to make the trivia non-optional and passing .zero
if we don’t want any? This question basically applies to all Trivia?
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The reason is that if we pass .zero
is replaces the leading Trivia
.
We expect
␣import SwiftSyntax
or
␣struct TestStruct {
}
But get
but get ␣importSwiftSyntax
␣struct TestStruct{
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, I see. Makes total sense. Could you add a comment describing the behaviour?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would some documentation like this help?
/// Builds list of `${kind}`s
/// - Parameter format: The `Format` to use.
/// - Parameter leadingTrivia: Replaces the the last leading trivia if not nil.
897f92f
to
fbbfb15
Compare
fbbfb15
to
396f756
Compare
I have address more less all off them, I have some small questions and added some comments.
Yes, there is one here: swiftlang/swift#36726 |
Let’s see what CI has to say about this. @swift-ci Please test |
🎉 CI passed. I’m proposing that we merge this as is and you address the following outstanding things in follow-up PRs:
|
396f756
to
9107c19
Compare
Yes! I just rebased on |
@swift-ci Please test |
If it didn’t come through when you put up the PR and during review, I wanted to give you a huge thank you for working on this and having the perseverance to see this patch through to the end (I only now realized that it was started more than a month ago). 🙏🏼 |
I love this work so much, thank you @kimdv! 🥳 |
You're welcome! |
From #263
Okay, now we are ready for the next step. 🚀
(I have modified this file, not sure if it is the right, but then we can start a conversation)
I'm a little confused on how I should approach this.
So the idea is that inside etc StructDeclSyntaxBuilder should not have
add*
but all possibilities should be basses within theinit
method?