How to implement a macro that generates a large RawRepresentable Enum?

Assume we have an enum like this:

enum Currency: String {
case usd = "USD"
case eur = "EUR"
case `try` = "TRY"
... 100 more lines ...
}

This enum is helpful when dealing with ISO-4217 currency codes, and may be generated fully automatically (which I've done via simple swift script).
Trying to implements a macro that could generate enum without any need to call any shell script build phases.
Can't figure out how can I achieve that. Tried different approaches:

  1. freestanding macro that simply returns a string that then is converted to ExprSyntax
public struct CurrenciesGenerateMacro: ExpressionMacro {
    public static func expansion(of node: some SwiftSyntax.FreestandingMacroExpansionSyntax, in context: some SwiftSyntaxMacros.MacroExpansionContext) throws -> SwiftSyntax.ExprSyntax {
        return """
               enum Currency: String {
               case adp = "ADP"
               case aed = "AED"
               }
               """
    }
}

(actually it's a bit more complicated, with a lookup to NSLocale.isoCurrencyCodes, but there's some weird error with is, I will mention it down below)

and the compiler warns me this doesn't work:

  1. tried to make an attached(member, names: arbitrary). but the compiler warns me there are no cases in enum for it to be a RawRepresentable:
@GenerateCurrencies
enum Currency: String { // <- 'Currency' declares raw type 'String', but does not conform to RawRepresentable and conformance could not be synthesized

}

but expanding a macro show all the cases are generated inside :thinking:

Also, when debugging my implementation, I've faced with an error I cannot clarify for me:

What is wrong here, hence the macro seemed to expand everything correctly?
The macro code is:

extension String {
    func wrappedInTicksIfNeeded() -> String {
        let keywords: Set<String> = ["for", "try", "let", "var"]
        return keywords.contains(self) ? "`\(self)`" : self
    }
}

public struct CurrenciesGenerateMacro: ExpressionMacro {
    public static func expansion(of node: some SwiftSyntax.FreestandingMacroExpansionSyntax, in context: some SwiftSyntaxMacros.MacroExpansionContext) throws -> SwiftSyntax.ExprSyntax {

        var result = "enum Currency: String {"

        NSLocale.isoCurrencyCodes.forEach { code in
            if let description = NSLocale.current.localizedString(forCurrencyCode: code) {
                result += "\n    /// \(description)"
            } else {
                result += "\n"
            }

            result += "\n    case \(code.lowercased().wrappedInTicksIfNeeded()) = \"\(code)\"\n"
        }

        result += "}"

        return ExprSyntax(stringLiteral: result)
    }
}

Tried to eliminate any parsing issues via tree builder, but cannot figure out how to return a tree as an ExprSyntax:

public struct CurrenciesGenerateMacro: ExpressionMacro {
    public static func expansion(of node: some SwiftSyntax.FreestandingMacroExpansionSyntax, in context: some SwiftSyntaxMacros.MacroExpansionContext) throws -> SwiftSyntax.ExprSyntax {

let syntax = TypeSyntax(
            EnumDeclSyntax(name: .identifier("Currency"),
                           inheritanceClause: InheritanceClauseSyntax(inheritedTypes: InheritedTypeListSyntax {
                InheritedTypeSyntax(type: IdentifierTypeSyntax(name: .identifier("String")))
            }),
                           memberBlock: MemberBlockSyntax {
                MemberBlockItemListSyntax {
                    for code in NSLocale.isoCurrencyCodes {
                        MemberBlockItemSyntax(decl:
                                                EnumCaseDeclSyntax(leadingTrivia: Trivia.lineComment(NSLocale.current.localizedString(forCurrencyCode: code) ?? code.uppercased())) {
                            EnumCaseElementListSyntax {
                                EnumCaseElementSyntax(name: .identifier(code.lowercased().wrappedInTicksIfNeeded()),
                                                      rawValue: InitializerClauseSyntax(value: StringLiteralExprSyntax(content: code)))
                            }
                        }
                        )
                    }
                }
            })

return ??
}

Why are you wrapping the EnumDeclSyntax into a TypeSyntax here? A type definition is a DeclSyntax, and TypeSyntax is type name, function signature, tuple type, etc.

IIRC currently top-level declarations by macro is forbidden, so you’ll need to use an attached MemberMacro to generate the members. You should return a list of EnumCaseDeclSyntax in a [DeclSyntax] in this case.

Thank you, will try [DeclSyntax].
As to MemberMacro - I mentioned it in the second scenario, the compiler is not allowing me to declare a RawRepresentable enum with no cases, even when a macro expands all the members correctly

Please double check the documentation for macro kinds and their corresponding function type. The compiler won’t expand the attribute or expression correctly if the types are mismatched.

Should the compiler pick the appropriate type when constructing from string literal?

  1. Could you tolerate "case UDD, EUR, ...", etc instead?

But I do a bigger picture question: I wonder what that would give you...

  1. The currency list is not getting changed frequently. If you are using this enum from your app in e.g. a switch, and add a new case (by whatever means) you'd need to add the corresponding case handling in the switch (unless you are having a default statement with, say, fatalError("TODO") in it, which will bring your attention to the issue at runtime so you'd still need to add a new case handler). And if you add a new case manually what has stopped you adding the case manually to the enum itself in the first place?

  2. Say you do have that enum in the app (manual or autogenerated). Check how many places you actually use "adp" or "aed" in the app – are there any at all? I guess there would be none... Typically apps don't need such enums.

Unless this is an exercise on how to use macros I don't see a point...

  1. will try that, but not sure it's possible (like the try case that I need to wrap in ticks)
  2. it's actually is updating annually, which is why it's autogenerated in shellscript right now. Most of the time I don't need to add a corresponding case, since I need this enum mostly to ensure the ISO-4217 code I got from backend is correct and may be used for price formatting later. (see my library for this)
  3. I have at least rub, usd and eur in my code for a fallback scenarios

Late reply, but you need a declaration macro to introduce new type definitions.

Like this:

@freestanding(declaration, names: named(Currency))
public macro generateCurrenciesEnum() = #externalMacro(
    module: "CurrencyCodeMacroMacros", 
    type: "GenerateCurrenciesEnumMacro"
)
import SwiftCompilerPlugin
import SwiftSyntax
import SwiftSyntaxBuilder
import SwiftSyntaxMacros

public struct GenerateCurrenciesEnumMacro: DeclarationMacro {
    public static func expansion(
        of node: some FreestandingMacroExpansionSyntax,
        in context: some MacroExpansionContext
    ) throws -> [DeclSyntax] {
        return [
            """
            enum Currency: String, CaseIterable {
                case adp = "ADP"
                case aed = "AED"
            }
            """
        ]
    }
}

@main
struct CurrencyCodeMacroPlugin: CompilerPlugin {
    let providingMacros: [Macro.Type] = [
        GenerateCurrenciesEnumMacro.self,
    ]
}

Usage:

import CurrencyCodeMacro

#generateCurrenciesEnum()

Just out of curiosity, what's the purpose of this versus a normal Swift package?
Either way, you're invoking SPM – and doing this via a macro, with all the additional ceremony, feels like using a sledgehammer to crack a nut, so to speak.

First of all, to find out how macros work
Second, to automate this enum at compiletime without need in any script phase

Ah, fair enough on the learning point.

What benefit do you get from compile time generation of this enum versus just putting it in a separate Swift package, though? That seems like a more appropriate use for something like this, and then you won't even need to incur the huge cost of building Swift Syntax.

It’s already a cocoapods pod, but country and currency codes are not that static as they seem. Nearly each major iOS release this enum is generated differently. So, I could use some stencil template with Sourcery. But this task looks like a candidate for swift macro

I'd be considering using a Swift package (or Cocoapods pod!) and just releasing new versions as time goes on and things change.

Macros are not really an appropriate tool for generating static code – their power comes from being able to operate on syntax they're provided, and can enhance by removing large amounts of boilerplate. I think SPM, Carthage, Cocoapods, etc. are much better – and much, much lighter* – for distributing static code like this.

Adding macros to your package imports Swift Syntax, which means all downstream consumers of your package (which may just be yourself, of course!) also now need to download and build that ~38,000 line of code project which can bloat CI times by 12 minutes or so on Xcode Cloud. If you're offering a macro that operates on syntax that a user provides – checking a URL is valid at compile time, enhancing a type to handle writing boilerplate, etc. – then that's a price maybe worth paying. If it's just generating static code though, it's almost certainly not worth it when you could just distribute that static code.

I plan to make a local package for myself, this will not affect a CI build time. Also, I find this approach a perfect study case for macros, next step is to replace some sourcery templates for type lenses :)
But I fully agree to your point, I just need to clarify for myself, what approaches I may use to achieve the desired effect, so I could choose the macro instead of other tools when appropriate

i use this technique quite often, you can see an example of it in the open source swift-unidoc project in the ISO.Macrolanguage type. i my view, it is a perfectly valid and legitimate use of macros.

putting the macro and its generated types in a separate SPM repo will have no impact on compile times; SPM must build dependencies from source, same as for the leaf project.

pre-baking the types (for example, by invoking the macro through a plugin) is essentially what everyone did in the pre-5.9 days with gyb, and i personally was disenchanted by that system. moreover, because SPM is SPM, there is a high chance it would still download the entire history of the swift-syntax repository to support the plugin unless the package author spent effort to prevent this from ocurring.

+1 . this stuff changes too frequently to justify a release on every change, but too slowly to justify making it completely dynamic and stringly-typed.