1

I'm trying to create a macro that makes use of some objects.

Suppose I have the following definitions:

trait Foo:
  def doStuff(): Unit

// in other files

object Bar extends Foo:
  def doStuff() = ...

object Qux extends Foo:
  def doStuff() = ...

(Foo is not sealed on purpose, see below)

I want to create a macro that has the following shape:

inline def runFoo(inline foo: Foo): Unit = ${ runFooImpl('foo) }

Such that when invoking runFoo with any singleton instance of Foo the corresponding doStuff method will be called at compile-time.

For example:

runFoo(Bar)

Will trigger Bar.doStuff() at compile-time.

In pseudo-code the macro would look something like this:

def runFooImpl(fooExpr: Expr[Foo]): Expr[Unit] = 
  val foo: Foo = fooExpr.valueOrAbort // does not compile

  foo.doStuff()

 '{ () }

Currently valueOrAbort cannot work, due to the lack of FromExpr.

My question is, is there some way to leverage the fact that Bar, Qux, etc. are compile-time constants to be able to extract them from the concrete Expr[Foo] during macro expansion?

Note that turning Foo into a sealed trait (and write a FromExpr by pattern matching on it) is not an acceptable solution, as I want Foo to be extensible by client code (with the restriction that all Foo implementations must be objects).

Thanks in advance

4
  • 1
    Try making Foo extend Singleton that forces all clients to use object - Not sure if that helps with the macro stuff tho. - BTW, what is the point of this other than just manually calling doStuff? Commented Jul 3 at 21:14
  • 1
    Good idea with Singleton, thanks. It communicates my intent better (although I can't extend it directly, it has to be a self-type). Unfortunately, I don't see how it helps with the macro itself (since the macro can figure that out even without explicitly adding it to the trait).
    – ncreep
    Commented Jul 3 at 22:26
  • 1
    As to why I want to call doStuff, the actual methods I have on those objects encapsulate different tree transformations (instances of TreeMap) that I want to apply to the code, and I want to let the client code choose which transformations to apply at the macro invocation site.
    – ncreep
    Commented Jul 3 at 22:28
  • 1
    Something like inline def runFoo[A <: Foo & Singleton](inline foo: A): Unit ? Mind that you still have to get the type, then the Symbol, turn it into Class and finally obtain an instance using reflection, while checking that the class is even available at the compile time (it won't be if user define that object in the same scope as macro expansion). Commented Jul 4 at 7:30

1 Answer 1

2

I've done that before. This approach requires several conditions:

  • you should make sure that user would actually pass object - you can do this with e.g. inline def runFoo[A <: Foo & Singleton]: Unit

  • you CANNOT prevent user from doing something like

    class Example1 {
      def localFunction: Unit = {
        object LocalDefinition extends Foo
        runFoo[LocalDefinition]
      }
    }
    
    class Example2 {
      object LocalDefinition extends Foo
      def localFunction: Unit = {  
        runFoo[LocalDefinition]
      }
    }
    

    and this cannot be implemented as objects - even though they are singletons - contains a references to enclosing classes. So this would still require a check inside a macro, and a compilation error with the message explaining why

  • in general - if you want to access object A extends Foo, there has to be Class[A$] available on the classpath that is available to the macro, so even things like

    object LocalDefinition extends Foo
    runFoo[LocalDefinition]
    

    is off limits, since you cannot obtain an instance of something which didn't emitted a bytecode yet (and it didn't since the file is still being compiled as evidenced by the ongoing macro expansion)

Once we accept these limitations, we might hack something together. I already prototyped something like that a few years ago and used the results in my OSS library to let users customize how string comparison should work.

You start by drafting the entrypoint to the macro:

object Macro:

  inline def runStuff[A <: Foo & Singleton](inline a: A): Unit =
    ${ runStuffImpl[A] }
    
  import scala.quoted.*
  def runStuffImpl[A <: Foo & Singleton: Type](using Quotes): Expr[Unit] =
   ???

Then, we might implement a piece of code that translates Symbol name into Class name. I'll use a simplified version which doesn't handle nested objects:

  def summonModule[M <: Singleton: Type](using Quotes): Option[M] =
    val name: String = TypeRepr.of[M].typeSymbol.companionModule.fullName
    val fixedName = name.replace(raw"$$.", raw"$$") + "$"
    try
      Option(Class.forName(fixedName).getField("MODULE$").get(null).asInstanceOf[M])
    catch
      case _: Throwable => None

with that we can actually use the implementation in macro (if it's available):

  def runStuffImpl[A <: Foo & Singleton: Type](using Quotes): Expr[Unit] = {
    import quotes.*
    summonModule[A] match
      case Some(foo) =>
        foo.doStuff()
        '{ () }
      case None =>
        reflect.report.throwError(s"${TypeRepr.of[A].show} cannot be used in macros")
  }

Done.

That said this macro typeclass pattern, as I'd call it, is pretty fragile, error-prone and unintuitive to user, so I'd suggest not using it if possible, and be pretty clear with explanation what kind of objects can go there, both in documentation as well as in error message. Even then it would be pretty much cursed feature.

I'd also recommend against it if you cannot tell why it works from reading the code - it would be pretty hard to fix/edit/debug this if one cannot find their way around classpaths, classloaders, previewing how Scala code translates into bytecode, etc.

3
  • Thank you for the detailed explanation! I was really hoping to avoid reflection. I was assuming that if I can invoke the object method statically from the macro, I'd be able to make the macro recognize it without reflection. I'll play around with the code you wrote though and see how it goes. Thanks
    – ncreep
    Commented Jul 4 at 10:14
  • 2
    Unfortunately not, Scala 2 had reify which basically allowed compile code within macro and obtain result of the compilation, but Scala 3 has none of that. @DmytroMitin made github.com/DmytroMitin/dotty-patched which allows that but it's an unofficial fork of the compiler, there is no official way of obtaining things that are not guaranteed to be present at compile time (basically primitives, Options, Eithers and Tuples of primitives, Options...). If you want to obtain the expression of a singleton type other than these, you are bypassing compiler checks. Commented Jul 4 at 10:23
  • I see, thanks again
    – ncreep
    Commented Jul 4 at 11:54

Not the answer you're looking for? Browse other questions tagged or ask your own question.