Pre-pitch: Freestanding extension macros

I raised this idea in another thread but that thread seems to have died out and I feel like this merits a little more discussion.

I suggested allowing macros to extend other types (not limited to the attached type, like now) provided the macro definition - or perhaps the call site - specifies what the extended type(s) will be.

The reason that's always given for the current restriction on extension macros only being allowed to extend the attached type is so the compiler can be smart about when to expand the macros. It seems to me like this idea would satisfy that requirement.

So I imagine something like this, where the macro definition includes the extended type:

@freestanding(extension, extending: EnvironmentValues, names: arbitrary)
public macro EnvironmentValue<T, K>(_ name: StaticString, type: T.Type, key: K.Type)
  = #externalMacro(module: "···", type: "EnvironmentValueMacro")

then this:
#environmentValue("myValue", type: Int.self, key: MyKey.self)

expands to the usual SwiftUI environment value property:

extension EnvironmentValues {
  var myValue: Int {
    get { self[MyKey.self] }
    set { self[MyKey.self] = newValue }
  }
}

And perhaps we could also allow the macro invocation to specify the extended type, like if you have a protocol where implementing it tends to be very boilerplate-ish:
#addMyProtocol(to: OtherType.self)

Does this seem workable? Is there any reason why either of these (specifying the extended type in declaration vs invocation) doesn't give the compiler the information it needs to intelligently and conservatively expand macros? What other considerations do we have?

5 Likes

Are you aware that it's possible to achieve the above using member role of attached macro on extension in Swift 5.9?

@EnvironmentValue
extension EnvironmentValues {}

I used it a lot in my code. IMO it's way more clear than the freehanding macro you suggested.

1 Like

Yes, but it forces the user of the macro to know which type is being extended, as well as the exact conditions in the where clause. One of the reasons to create a macro for implementing an extension is specifically to abstract away the boilerplate of the where clause or to automatically determine which type needs to be extended.

In one of my library, I reached to macro to generate several types and realised it was not possible to do so.

Something like:

#GenerateType(MyType, raw: Int, ...)

would generate

struct MyType {
   let raw: Int
   ...
}

Note that the type generated type was not only parametrize on type but also some cardinality etc.
At the end I used the classic code generation tools.

Thanks. This fits my use case perfectly.

However, I did notice one odd thing when I tried it: If the right brace is on the same line as the left, as show, the added decls appeared outside of the extension:

@IsOk
extension Foo{}
  var isOk: Bool {
   amount >= 0
  }

But this succeeds:

final class IsOkBugTests: XCTestCase {

  func testMacro() throws {
    assertMacroExpansion(
            """
            @IsOk
            extension Foo {}
            """,
            expandedSource: """
            extension Foo {

                var isOk: Bool {
                    amount >= 0
                }
            }
            """,
            macros: testMacros
    )

  }
}

This works if the right brace is on the next line.

@IsOk
extension Foo {
}
let foo = Foo(amount: 1)
print(foo.isOk)
// -> true