[Pitch] Macro "Extensions"

Hey all!

My pitch is for libraries that provide macros to allow the consuming end to provide custom configurations/values for the macro to use at compile time.

I have a vague idea of how this could be done and really want to take on the challenge of putting together and documenting a POC. Before I do, though, I wanted to gauge interest here and see if this is even feasible or if maybe this is a lot cooler in my head than it is outside of it (likely).

The Problem

I'm building a macro which would greatly benefit from the ability to accept a custom transform(_:)-type function. Something that I might receive from an initialization function in a traditional swift package (ie. LibraryName(transform: { $0 * 10 }))

However, due to the nature of macros, this is not an option. At least not as far as I'm aware.

My proposed solution would look something like this:

Library-Side

Library developers can annotate specific properties:

struct MyMacroStruct {
    ...
    @ConsumerProvided
    let transform: TransformClosure = { ... }
    ...  
  }

or entire structs:

@ConsumerProvided
struct MyMacroConfig {
    let transform: TransformClosure = { ... }
    let someVal: String = "..."
}

Consumer-Side

Library consumers can provide these config values which will override the default implementation in the library:

@Providing(for: MyMacroModule)
let transform: TransformClosure = { ... }

or

@Providing(for: MyMacroModule)
let config: MyMacroConfig = .init(
    transform: { ... }
)

or even (maybe, spitballing)

#Providing(for: MyMacroModule) {
    let transform: TransformClosure = { ... }
    let someVal: String = "..."
}

These values must be declared in a file separate from the rest of the project. That is to say, a consumer must not declare these provided values within a file that is compiled normally as part of the greater project. In fact, the file will not be compiled as part of the project at all, and therefore has no access to any part of the project. This means all provided values must be some kind of literal, since they will be taken verbatim and substituted in the library in place of the default value(s). The compiler would be aware of this and display errors if there is a type mismatch or invalid symbol name

Alternative Solution

@freestanding(expression) public macro MyMacro(_ transform: TransformClosure) -> String = #externalMacro(module: "MyLibMacros", type: "MyMacro")

and consumer-side:

extension MyMacro {
    let transform: TransformClosure = ...
}

where the extension just provides a default value for the transform parameter in the macro. Thus, instead of:

let something = #MyMacro({ ... })

we must only say:

let something = #MyMacro

Without getting too deep in the weeds surrounding the how, what do you think? Could you imagine a better solution to the problem? Did I miss something in the docs and all of this was for nothing?

1 Like

Hi there. I'm not sure I'm fully onboard with the proposal, but I think the problem it addresses is one that deserves some attention.

I solved a sorta-kinda similar (though much simpler) problem in a macro of my own by having the macro emit code that just expects a certain global config symbol to exist, and that's where users of the macro (which is just me, since it's not public) provide "input" to the macro.

So you'd invoke the macro (in that case, an expression macro) like normal: #doXYZThing(), and it emits something like f(g(xyzConfig.clientProvidedThing)). Naturally, that leads to compiler errors until the macro user defines a global let xyzConfig = XYZNormalStruct()

There are lots of situations where that wouldn't work (and maybe some people would think that it's icky to make "random assumptions" about the environment the macro is being expanded into, but for me, I needed a one-time setup, and I needed it to be threadsafe, so a global let was an easy, safe solution