swiftswift-macroswift-syntax

Swift Macro CodeBlockItemListSyntax does not generate line separators?


I am writing a macro that generates a code block that mostly consists of static content, with a part in the middle that is dynamic depending on the macro arguments. Here is an example to illustrate:

enum ExampleMacro: ExpressionMacro {
    static func expansion(
        of node: some FreestandingMacroExpansionSyntax,
        in context: some MacroExpansionContext
    ) throws -> ExprSyntax {
        let dynamicPart = CodeBlockItemListSyntax {
            // I am using Int.random(..) to show that this part is dynamic
            // in the real code it won't be Int.random(...) of course
            for i in 0..<Int.random(in: 10..<20) {
                "var value\(raw: i) = 0"
            }
        }
        return """
        someFunction {
            // ... static portion that's always there...
            \(dynamicPart)
            // ... static portion that's always there...
        }
        """
    }
}
@freestanding(expression)
public macro example() = #externalMacro(module: "MyMacroMacros", type: "ExampleMacro")

Unexpectedly, #example expands to

someFunction {
    // ... static portion that's always there...
    var value0 = 0var
    value1 = 0var
    value2 = 0var
    value3 = 0var
    value4 = 0var
    value5 = 0var
    value6 = 0var
    value7 = 0var
    value8 = 0var
    value9 = 0
    // ... static portion that's always there...
}

giving me many compiler errors about "0var".

How can I fix this?


Solution

  • When you add a whitespace or a semicolon to the end of the line, it works just fine. But I think I know what causes this. Look at the string below:

    "var value\(raw: i) = 6 func foo() {}"
    

    When I input it to the CodeBlockItemListSyntax the macro generates this:

    var value0 = 6
    func foo() {
    }
    

    Did you see what it did? It automatically indented the code for you. It also does the same thing with the semicolon (and also escape sequences?), too:

    "var value\(raw: i) = 6;func foo() {}"
    

    Into:

    var value0 = 6;
    func foo() {
    }
    

    I think what CBILS doest is just stash the string literals side by side (using your input):

    "var value1 = 0var value2 = 0var value3 = 0"
    

    When swift tries to parse this it does it like so:

    (var value1 = 0var) (value2 = 0var) (value3 = 0)
     ┬────────────┬───  ┬──────────┬──   ─┬────────
     |            ╰some |          ╰─some ╰ set value
     ╰─ var init.  value╰─ set value  value
    

    And when swift tries to indent this it puts a line break between every statement (in parenthesis), so the end result becomes:

    var value1 = 0var
    value2 = 0var
    value3 = 0
    

    But if you make the value a string literal an instead of an integer literal it works fine. Why is that?

    Because anything that has a start and an end (terminating) (e.g. () "" [] {}) has no possibility of intersecting with something (e.g. ""abc -> ("")(abc))

    In short terms:

    The developer for this library has forgot to put seperators between the code blocks. So put a whitespace or a semicolon at the end to fix this issue. And report the bug to the authors. :)