Skip to content

Initial implementation of declarative and type-safe wrapper #139

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
Sep 5, 2019

Conversation

akkyie
Copy link
Contributor

@akkyie akkyie commented Aug 7, 2019

Implemented declarative and type-safe wrappers around SwiftSyntax, using @_functionBuilder.

(@akyrtzi thank you for encouraging me to upstream it!)

Example Usage

let format = Format(indentWidth: 2)

let sourceFile = SourceFile {
  Import("SwiftSyntax")

  Struct("ExampleStruct") {
    Let("syntax", of: "Syntax")
  }
}

let syntax = sourceFile.buildSyntax(format: format, leadingTrivia: .zero)

var text = ""
syntax.write(to: &text)
print(text)

emits:

import SwiftSyntax
struct ExampleStruct {
  let syntax: Syntax
}

Currently implemented

  • Integer and string literals (w/o interpolation) (3781538)
  • let and var with optional initializer (edad18d)
  • struct with member definition (w/o attributions, access control, generics etc.) (e8f2d02)
  • import (w/o importing individual declarations) (1687304)
  • Source file (4495871)
  • Changing indent width (using tabs is not yet)

Considarations

  • Since they are built on top of @_functionBuilder, calling them "SyntaxBuilders" sounds natural to me. But we already have SyntaxBuilders; can this conflict be a problem? I'm welcome to change my code if we have a good way to call them.
  • I think they can provide some syntactic safety, not semantic safety. But how do we validate the input, or should not? Should we accept something like Import("") or Let("===", of: "@@@")?
  • I consider strict formatting to be also out of scope, and the output of my "SyntaxBuilders" can have an opinionated format. We can provide some basic options such as changing indent width, and further formatting will be done by piping the output to swift-format or other formatters which supports SwiftSyntax.
  • I implemented them by hand because I think the structure and parameters of each builder don't have to exactly match the syntax tree of SwiftSyntax for better developer ergonomics. Do you have any idea to gyb-generate any part of (or, possibly, entire) the implementation?

And I'm a complete newbie here. Let me know if I have to change anything!

Copy link
Contributor

@nkcsgexi nkcsgexi left a comment

Choose a reason for hiding this comment

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

@akkyie Welcome to the project and this change looks awesome! Some nit-picky comments are inline.

Package.swift Outdated
}

package.products.append(.library(name: "SwiftSyntaxBuilder", targets: ["SwiftSyntaxBuilder"]))
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 put this alone with SwiftSyntax? We'd like it to be a dynamic library when building from build script and a static library otherwise.

Copy link
Contributor

Choose a reason for hiding this comment

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

Should we have this functionality as part of the SwiftSyntax library instead of a separate SwiftSyntaxBuilder library? It will simplify the clients since they won't have to import two separate modules.

Copy link
Member

Choose a reason for hiding this comment

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

Since Swift doesn't yet permit dead-stripping anything that's declared public in external modules, adding more code to a module forces its binary size increase on everyone, whether the new APIs are used or not.

So in a case like this, where we have something that's adding value as a layer on top of the underlying library, but isn't critical functionality in and of itself, I think it makes sense to keep it a separate module that folks opt in to.

Copy link
Contributor

Choose a reason for hiding this comment

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

To clarify, if a library is statically linked with an executable, won't SwiftPM do dead-strip on it and remove public APIs that are not used in the executable? \cc @aciidb0mb3r

Copy link
Member

Choose a reason for hiding this comment

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

Unless the situation has changed since I last looked into it, anything declared as open or public is lowered by IRGen as @llvm.used, preventing it from ever being dead-stripped by the linker (SR-1021). That makes sense in a dylib/framework situation where you don't know what the eventual consumer is using, but ends up also applying to statically linked executables.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah interesting, I wasn't aware of that.

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 was just not sure I should do so; did in bd31ddb.

Copy link
Contributor

Choose a reason for hiding this comment

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

Let's leave it as a separate library while it's still in flux, then we can revisit if we want it to become SwiftSyntax API later.

public typealias Let = Variable<VariableLetMutability>
public typealias Var = Variable<VariableVarMutability>

public struct Variable<Mutability: VariableMutability>: DeclBuildable {
Copy link
Contributor

Choose a reason for hiding this comment

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

Using generic types here for mutability seems to me an overkill. Can we put let or var keyword as part of the initializer?

Copy link
Contributor Author

@akkyie akkyie Aug 10, 2019

Choose a reason for hiding this comment

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

Do you mean it should look like Variable(let: "something")? I think it's more natural if we can just write Let("something") and Var("something"), and afaik we need separate types (or a generic type) for this way.

import SwiftSyntax

@_functionBuilder
public struct DeclListBuilder {
Copy link
Contributor

Choose a reason for hiding this comment

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

This could be an internal type; so is the buildBlock function.


import SwiftSyntax

public struct StringLiteral: ExprBuildable {
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 merge all ExprBuildables into a single file? something like ExprBuildables.swift? along with the protocols.


import SwiftSyntax

public struct Struct: DeclBuildable {
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 merge all DeclBuildables into a single file? something like DeclBuildables.swift? along with the protocols.

import SwiftSyntax

@_functionBuilder
public struct SyntaxListBuilder {
Copy link
Contributor

Choose a reason for hiding this comment

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

This type and the buildBlock function could both be internal.

Copy link
Contributor Author

@akkyie akkyie Aug 10, 2019

Choose a reason for hiding this comment

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

Merged into files in f2ef721, and removed public modifiers in fe3a245.

@akkyie
Copy link
Contributor Author

akkyie commented Aug 14, 2019

Sorry for addition during review, but I fixed implementation to handle leading trivia (7b0e51d) along with tests for it (232a4aa), which is needed to support Comment(akkyie#2).

let initializer: ExprBuildable?

public init(_ name: String, of type: String, value: ExprBuildable? = nil) {
self.name = name
Copy link
Contributor

Choose a reason for hiding this comment

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

I think type being String here is not great, but it's a good first step. I'd like to see more declarative wrappers around building types alongside declarations and expressions, and those APIs can eventually subsume this one.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Agreed. In fact, I made a Type type to express a type when it was just my toy, but couldn't figure it out how a straightforward API should look like.

Copy link
Contributor

@harlanhaskins harlanhaskins left a comment

Choose a reason for hiding this comment

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

This is good to land, we'll want to talk about the high-level API of how we want this to look, from a broad perspective, before committing to it. Thanks for making this, @akkyie!

@akyrtzi
Copy link
Contributor

akyrtzi commented Aug 29, 2019

@swift-ci Please test

@swift-ci
Copy link
Contributor

Build failed
Swift Test OS X Platform
Git Sha - 232a4aa

@akyrtzi
Copy link
Contributor

akyrtzi commented Aug 29, 2019

CI shows this error:

/Users/buildnode/jenkins/workspace/swift-syntax-PR-macOS/branch-master/swift-syntax/Tests/SwiftSyntaxBuilderTest/FormatTests.swift:4:18: error: module 'SwiftSyntaxBuilder' was not compiled for testing
@testable import SwiftSyntaxBuilder
                 ^

@akyrtzi
Copy link
Contributor

akyrtzi commented Aug 30, 2019

I'd suggest to try removing @testable and only test the public APIs.

@akkyie
Copy link
Contributor Author

akkyie commented Sep 1, 2019

I agree that we should test public APIs. I added @testable not to test an internal API, but to use Format.indented which I don't see any reason to be used outside the implementation and testing.

I'm wondering if I can fix it by adding -Xswiftc -enable-testing to swift test in the build script:
https://github.com/apple/swift-syntax/blob/bc8a30794279c63b637a8918eb3c5eef75f714c5/build-script.py#L165-L172

Could you tell me if there is any reason not to do so?

@benlangmuir
Copy link
Contributor

I don't like -enable-testing, because it means you are pessimizing the release build by treating all the internal symbols as public (increased binary size, reduced ability to optimize).

You can avoid that affecting the final toolchain by testing with -enable-testing and then doing the final release build for installation without it, but then you can't share the build between testing and install (unlike the rest of building swift), and you're not testing exactly the same configuration you ship. You can avoid -enable-testing entirely by using underscored public methods for the small number of symbols you actually need to expose. That's what I did in sourcekit-lsp.

@akkyie
Copy link
Contributor Author

akkyie commented Sep 4, 2019

@benlangmuir Thank you for your explanations! That convinced me. Made some API public and underscored, and testing with the release build has succeeded on my machine.

@nkcsgexi I made a revert 9b5a8ad as tests could not be built without making function builders public.

SourceFileTests.swift:10:33: error: 'buildBlock' is inaccessible due to 'internal' protection level
    let sourceFile = SourceFile {
                                ^
<unknown>:0: note: 'buildBlock' declared here

@akyrtzi
Copy link
Contributor

akyrtzi commented Sep 4, 2019

@swift-ci Please test

@swiftlang swiftlang deleted a comment from swift-ci Sep 4, 2019
@akyrtzi
Copy link
Contributor

akyrtzi commented Sep 4, 2019

There is a bit of "churn" with all these commits in the PR, do you mind squashing them to one commit and doing a force push?

@SDGGiesbrecht
Copy link

@akyrtzi, are you aware of the “Squash and merge” button? (Click the down arrow beside “Merge”.)

It has the same end effect for the commit history on master, but since it doesn’t alter the pull request, the request’s own commit timeline still corresponds to the conversation and reviews if anyone needs to look back at them in the future.

It also avoids the issues force pushing can cause across clones, even when the branch in question was never used.

@akyrtzi
Copy link
Contributor

akyrtzi commented Sep 4, 2019

Does it preserve the original author? My concern is that if I do the "squash and merge" it will squash and make me the author of the commit instead of @akkyie

@SDGGiesbrecht
Copy link

SDGGiesbrecht commented Sep 4, 2019

I hadn’t thought much about that until now. From what I can tell, it results in “X authored and Y committed”, a subset of GitHub’s co‐author functionality. But I don’t know how transferable that is to other repository inspection tools.

@akkyie akkyie force-pushed the swift-syntax-builder branch from 9b5a8ad to 3dc22c9 Compare September 5, 2019 01:04
@akkyie
Copy link
Contributor Author

akkyie commented Sep 5, 2019

@akyrtzi Squashed them into 3dc22c9 :)

@akyrtzi
Copy link
Contributor

akyrtzi commented Sep 5, 2019

@swift-ci Please test

@swiftlang swiftlang deleted a comment from swift-ci Sep 5, 2019
@akyrtzi akyrtzi merged commit 7fd8a8b into swiftlang:master Sep 5, 2019
@akkyie
Copy link
Contributor Author

akkyie commented Sep 5, 2019

Thank you everyone so much, for all of your reviews and advice! 🙌

@akkyie akkyie deleted the swift-syntax-builder branch October 22, 2019 15:57
adevress pushed a commit to adevress/swift-syntax that referenced this pull request Jan 14, 2024
…ized_exprs

Group the closing paren of exprs into the enclosed open/close breaks.
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.

8 participants