Strategies for passing an enum value as a macro parameter and reading during expansion?

Hi! Does anyone have experience passing an arbitrary enum value as an argument to a macro and then making use of that enum value in the macro implementation? I have a hack that seems to work (for now)… but I'm happy to hear about any more legit solutions that might be out there.

The SwiftData.Query family of macros has one that takes a SortOrder as a parameter:[1]

@attached(accessor) @attached(peer, names: prefixed(`_`))
macro Query<Value, Element>(
    filter: Predicate<Element>? = nil,
    sort keyPath: KeyPath<Element, Value?>,
    order: SortOrder = .forward,
    transaction: Transaction? = nil
) where Value : Comparable, Element : PersistentModel

What's interesting about SortOrder is that (AFAIK) this does not come defined with a "raw value"[2] like a String or Int:[3]

public enum SortOrder: Hashable, Codable, Sendable

My question then is what kind of strategies are available to pass this as an argument to a macro and then for the engineer building the macro to take conditional logic.

Here is a hack from the swift-syntax examples that shows what I'm trying to do:

public enum MetaEnumArgument {
  case left
  case right
}

@attached(member, names: named(Meta))
public macro MetaEnum(arg: MetaEnumArgument) = #externalMacro(module: "MacroExamplesImplementation", type: "MetaEnumMacro")

I can then attempt to call that in a test:

@MetaEnum(arg: .left) enum Cell {
  case integer(Int)
  case text(String)
  case boolean(Bool)
  case null
}

And my MetaEnumMacro implementation can open that up with code like this:

enum MetaEnumArgument: String {
  case left
  case right
}

init(node: AttributeSyntax, declaration: some DeclGroupSyntax, context: some MacroExpansionContext) throws {
  ...

  if case let .argumentList(arguments) = node.arguments,
     let argument = arguments.first,
     let expression = argument.expression.as(MemberAccessExprSyntax.self) {
    let text = expression.declName.baseName.text
    if let value = MetaEnumArgument(rawValue: text) {
      print(value)
    }
  }

  ...
}

I run that test and confirm that left is printing out. This is good… and I guess I could use this as a workaround. But it feels kind of hacky and clunky and I would also be interested to hear if anyone has any other ideas about that.

Here is a diff if you are interested in trying locally. Thanks!

diff --git a/Examples/Sources/MacroExamples/Implementation/MetaEnumMacro.swift b/Examples/Sources/MacroExamples/Implementation/MetaEnumMacro.swift
index 780a1f2c..856c5766 100644
--- a/Examples/Sources/MacroExamples/Implementation/MetaEnumMacro.swift
+++ b/Examples/Sources/MacroExamples/Implementation/MetaEnumMacro.swift
@@ -21,6 +21,11 @@ public struct MetaEnumMacro {
   let access: DeclModifierListSyntax.Element?
   let parentParamName: TokenSyntax
 
+  enum MetaEnumArgument: String {
+    case left
+    case right
+  }
+
   init(node: AttributeSyntax, declaration: some DeclGroupSyntax, context: some MacroExpansionContext) throws {
     guard let enumDecl = declaration.as(EnumDeclSyntax.self) else {
       throw DiagnosticsError(diagnostics: [
@@ -28,6 +33,15 @@ public struct MetaEnumMacro {
       ])
     }
 
+    if case let .argumentList(arguments) = node.arguments,
+       let argument = arguments.first,
+       let expression = argument.expression.as(MemberAccessExprSyntax.self) {
+      let text = expression.declName.baseName.text
+      if let value = MetaEnumArgument(rawValue: text) {
+        print(value)
+      }
+    }
+
     parentTypeName = enumDecl.name.with(\.trailingTrivia, [])
 
     access = enumDecl.modifiers.first(where: \.isNeededAccessLevelModifier)
diff --git a/Examples/Sources/MacroExamples/Interface/Macros.swift b/Examples/Sources/MacroExamples/Interface/Macros.swift
index 609ceaf9..f4571efc 100644
--- a/Examples/Sources/MacroExamples/Interface/Macros.swift
+++ b/Examples/Sources/MacroExamples/Interface/Macros.swift
@@ -126,6 +126,14 @@ public macro CaseDetection() = #externalMacro(module: "MacroExamplesImplementati
 @attached(member, names: named(Meta))
 public macro MetaEnum() = #externalMacro(module: "MacroExamplesImplementation", type: "MetaEnumMacro")
 
+public enum MetaEnumArgument {
+  case left
+  case right
+}
+
+@attached(member, names: named(Meta))
+public macro MetaEnum(arg: MetaEnumArgument) = #externalMacro(module: "MacroExamplesImplementation", type: "MetaEnumMacro")
+
 @attached(peer)
 public macro CodableKey(name: String) = #externalMacro(module: "MacroExamplesImplementation", type: "CodableKey")
 
diff --git a/Examples/Tests/MacroExamples/Implementation/MetaEnumMacroTests.swift b/Examples/Tests/MacroExamples/Implementation/MetaEnumMacroTests.swift
index 9e852839..ba9ecb1f 100644
--- a/Examples/Tests/MacroExamples/Implementation/MetaEnumMacroTests.swift
+++ b/Examples/Tests/MacroExamples/Implementation/MetaEnumMacroTests.swift
@@ -24,7 +24,7 @@ final class CaseMacroTests: XCTestCase {
 
   func testBasic() throws {
     let sf: SourceFileSyntax = """
-      @MetaEnum enum Cell {
+      @MetaEnum(arg: MetaEnumArgument.left) enum Cell {
         case integer(Int)
         case text(String)
         case boolean(Bool)


  1. Query(filter:sort:order:transaction:) | Apple Developer Documentation ↩︎

  2. Documentation ↩︎

  3. swift-foundation/Sources/FoundationEssentials/SortComparator.swift at f299bde9dff2b1ab45f360f8a6d8479f96b3bec6 · apple/swift-foundation · GitHub ↩︎

Since the value of the enum is a runtime value but the macro is working on the source code level, what you are doing seems to make sense to me. I don’t have a better idea.

2 Likes

@ahoppen Sounds good. Thanks!