Announcing SwiftSyntaxBuilder

Hi Swift Community!

SwiftSyntaxBuilder a new library that makes it possible to generate Swift code in a declarative way using result builders. :tada:

The library contains everything to get started generating Swift code with a nice and convenient API, but there is space for improvements!

I would like to encourage everyone interested to give SwiftSyntaxBuilder a try, report any feedback you have or contribute to the library. Any contributions would be amazing and this is a community driven library so if you want to get involved, please do! :pray:

Based on your feedback, we will be working on finding the missing parts and implementing them.

If you find a bug or have a suggestion to improve the API, you can find the guide to report them here.

Thanks to Akio Yasui for the initial version.

Thanks!

Kim

29 Likes

I think I found an example of this new library here: swift-syntax/CodeGenerationUsingSwiftSyntaxBuilder.swift at main · apple/swift-syntax · GitHub

Looks pretty neat!

3 Likes

Congratulations on the initial take on the lib! This is awesome and really a lifesaver for source generation plugins :clap: I've been cheering on this effort from the sidelines ever since :smiley:

For better or worse, for my current work we moved away from source generation but the shape of the lib and APIs looks excellent, exactly what one would have hoped for - congrats and thanks for your work!

5 Likes

This looks great, thank you for sharing!

I wish we had more documentation for it. This is what I found apart from the link shared by Kiel:
https://speakerdeck.com/akkyie/swiftsyntaxbuilder-equals-swiftsyntax-x-function-builders?slide=25 (in Japanese)

Is there any way to manipulate SourceFile from some other part of code other than main?

In other words as per the shared example the complete structure is generated within main function:

@main
struct Main {
var source = SourceFile

  static func main() {
    let properties = [
      "firstName": "String",
      "lastName": "String",
      "age": "Int",
    ]

    source = SourceFile {
      StructDecl(identifier: "Person") {
        for (propertyName, propertyType) in properties {
          VariableDecl("var \(propertyName): \(propertyType)")

          FunctionDecl("""
            func with\(propertyName.withFirstLetterUppercased())(_ \(propertyName): \(propertyType)) -> Person {
              var result = self
              result.\(propertyName) = \(propertyName)
              return result
            }
            """
          )
        }
      }
    }

    print(source.formatted().description)
  }
}

Is it possible to modify it say on tap of button:

@IBAction func addAddressProperty() {
   // add address property to source

print(source.formatted().description) // prints updated structure
}

You can perform this kind of code generation anywhere in your application. It is not bound to a main function. The examples in the swift-syntax repository are supposed to be readily executable. That's why they perform all the generation and transformation steps right in the main function.

Speaking about transformations, you can also rewrite an existing piece of code implementing a SyntaxRewriter (available in the SwiftSyntax module). AddOneToIntegerLiterals.swift is an example for that. To get from source code to an AST you want to use the Parser provided by SwiftParser.

1 Like

You should also be able to find documentation here: SwiftSyntax – Swift Package Index

If there is any kind of specific documention you need feel free to ask here :partying_face:

2 Likes

Is there some doc available beyond the code sample?

Using the syntax builder library is quite difficult at the moment without doc and the compiler errors for ResultBuilder are not always very clear,

For example: how to add protocol conformance to a struct.

I think I'm close, but this still doesn't compile

            StructDeclSyntax(name: "MyStruct") {

                InheritanceClauseSyntax(inheritedTypes: InheritedTypeListSyntax {
                    DeclSyntax("Sendable")
                    DeclSyntax("Codable")
                })

Static method 'buildExpression' requires that 'InheritanceClauseSyntax' conform to 'DeclSyntaxProtocol'

I have the same question for adding public or private modifiers

I would love a couple of additional code examples for common use cases : declaring struct with modifiers, protocol and generics, functions, control statements (if, switch, loops). Declaring and using enums

The block can only contain members of the structs. Other details must be specified in the initializer for StructDeclSyntax. For example, it has a parameter inheritanceClause which you need to pass the inheritance clause to. Members can only be declarations. That’s somehow what the compiler mentions: InheritanceClauseSyntax is not a DeclSyntax.

Regarding examples: The SwiftSyntax repository uses SwiftSyntaxBuilder intensively to generate code.

Thank you Danny. I'll continue to search the source code and the Tests to figure out the correct syntax

Sorry, but I’m only typing on a phone. Otherwise I’d have given you a full working example. Perhaps check the initializer to get an idea of all possible parameters.

No worries :-) I found a few examples.

Here is what I have so far

let defaultInheritance = InheritanceClauseSyntax {
            InheritedTypeSyntax(type: TypeSyntax("Codable"))
            InheritedTypeSyntax(type: TypeSyntax("Sendable"))
        }
        
let source = SourceFileSyntax {
            //TODO: is there a result builder for DeclModifierSyntax?
            StructDeclSyntax(modifiers:  DeclModifierListSyntax { DeclModifierSyntax(name: .keyword(.public)) },
                             name: "MyStruct",
                             inheritanceClause: defaultInheritance) {

Hi @sebsto. I would suggest to use the SyntaxBuilder initializers, they should be those with the header parameter if I remember correctly (typing from my phone as well so I can’t check). Then you can simply do

StructDeclSyntax("struct MyStruct: Codable, Sendable") {
  "let myProp: Int"
}

Basically the brackets of the trailing closure represents the brackets of the declaration you will get as output.

Another example:

let source = try SourceFileSyntax {
    try StructDeclSyntax("struct MyStruct: Codable, Sendable") {
        "let myArray: [Int]"
        "let myEnum: MyNestedEnum"
        
        try FunctionDeclSyntax("func bar() async throws -> Int") {
            "var result = 0"
            try ForStmtSyntax("for elem in myArray") {
                "result += elem"
            }
            "return result"
        }
        
        try EnumDeclSyntax("enum MyNestedEnum: String, Codable, Sendable") {
            "case a"
            "case b"
            for other in ["c", "d", "e"] {
                "case \(raw: other)"
            }
        }
    }
}

print(source.formatted())

Output:

struct MyStruct: Codable, Sendable {
    let myArray: [Int]
    let myEnum: MyNestedEnum
    func bar() async throws -> Int {
        var result = 0
        for elem in myArray {
            result += elem
        }
        return result
    }
    enum MyNestedEnum: String, Codable, Sendable {
        case a
        case b
        case c
        case d
        case e
    }
}

EDIT:
For this specific example, you can skip the whole FunctionDeclSyntax and just write the following as a multi line string

"""
func bar() async throws -> Int {
        var result = 0
        for elem in myArray {
            result += elem
        }
        return result
    }
"""

but once you need to add custom logic, swift-syntax will emit warnings and format the output with the wrong indentation if a parsed string isn't a valid declaration, so for example the following would emit warnings and the result would probably be wrongly formatted:

"for elem in myArray {"
if someCondition {
  "result += elem"
} else {
  "result -= elem"
"}"

This happens because when swift-syntax parses "for elem in myArray {" it expects to find a closing bracket in the same string, but it doesn't

2 Likes

Thank you Alessio - that helps.
I like the strong typing provided by

StructDeclSyntax(modifiers:  DeclModifierListSyntax { DeclModifierSyntax(name: .keyword(.public)) },
                             name: "MyStruct",
                             inheritanceClause: defaultInheritance) {

But your way of doing things helped me to progress