Search code examples
scalaannotationsmetaprogrammingscala-macrosscala-3

How to generate a class in Dotty with macro?


Is it possible to generate a new class with macro in Dotty, Scala 3 ?

Zlaja


Solution

  • Currently in Dotty there is only (kind of) def macros. Currently there is no (kind of) macro annotations, which could generate a new member, new class etc.

    For generation of a new member, new class etc. you can use

    Let me remind you that even in Scalac the ability to generate a new member, new class etc. also appeared not from the very beginning. Such functionality (macro annotations) appeared as Macro Paradise compiler plugin to Scalac.

    I can't exclude that somewhen somebody will write something like Macro Paradise for Dotty. It's too early for that, it's only feature-freeze for Dotty now, even language syntax (for example) and standard library keep changing now (there is also list of libraries that are testing their ability to work with Dotty, for example currently no Scalaz/Cats are there).


    For example, while in Scala 2 Simulacrum was using macro annotations, in Scala 3 Simulacrum-Scalafix is implemented as Scalafix rules

    https://github.com/typelevel/simulacrum-scalafix

    https://index.scala-lang.org/typelevel/simulacrum-scalafix/simulacrum-scalafix/0.5.0?target=_2.12

    One more use case: Breeze uses sbt plugin and Scalameta for source code generation

    https://github.com/scalanlp/breeze

    https://github.com/scalanlp/breeze/blob/master/DEVELOP.md

    https://github.com/dlwh/sbt-breeze-expand-codegen


    Update. We can now generate an inner class with Scala 3 (def) macros: Method Override with Scala 3 Macros


    Update (March 2023). Starting from Scala 3.3.0-RC2, there appeared macro annotations (implemented by Nicolas Stucki).

    https://github.com/lampepfl/dotty/releases/tag/3.3.0-RC2 (discussion)

    [Proof of Concept] Code generation via rewriting errors in macro annotations https://github.com/lampepfl/dotty/pull/16545

    Zhendong Ang. Macro Annotations for Scala 3 (master thesis) https://infoscience.epfl.ch/record/294615

    Macro annotation (part 1) https://github.com/lampepfl/dotty/pull/16392

    Macro annotations class modifications (part 2) https://github.com/lampepfl/dotty/pull/16454

    Enable returning classes from MacroAnnotations (part 3) https://github.com/lampepfl/dotty/pull/16534

    New definitions are not visible from outside the macro expansion.

    build.sbt

    scalaVersion := "3.3.0-RC3"
    

    Here is an example. Macro annotation @genObj generates a companion object with given tc: TC[A] = new TC[A], @modifyObj modifies the companion object generating given tc: TC[A] = new TC[A] inside:

    import scala.annotation.{MacroAnnotation, experimental}
    import scala.quoted.*
    
    object Macros:
      class TC[T]
    
      @experimental
      class genObj extends MacroAnnotation:
        def transform(using Quotes)(tree: quotes.reflect.Definition): List[quotes.reflect.Definition] =
          import quotes.reflect.*
          tree match
            case ClassDef(className, _, _, _, _) =>
              val modParents = List(TypeTree.of[Object])
    
              val tpe = TypeRepr.of[TC].appliedTo(List(tree.symbol.typeRef))
    
              def decls(cls: Symbol): List[Symbol] = List(
                Symbol.newVal(cls, "tc", tpe, Flags.Given, Symbol.noSymbol)
              )
    
              val mod = Symbol.newModule(Symbol.spliceOwner, className, Flags.EmptyFlags, Flags.EmptyFlags,
                modParents.map(_.tpe), decls, Symbol.noSymbol)
              val cls = mod.moduleClass
              val tcSym = cls.declaredField("tc")
              val tcDef = tpe.asType match
                case '[TC[t]] => ValDef(tcSym, Some('{new TC[t]}.asTerm))
              val (modValDef, modClsDef) = ClassDef.module(mod, modParents, body = List(tcDef))
    
              val res = List(tree, modValDef, modClsDef)
              println(res.map(_.show))
              res
            case _ =>
              report.errorAndAbort("@genObj can annotate only classes")
    
      @experimental
      class modifyObj extends MacroAnnotation:
        def transform(using Quotes)(tree: quotes.reflect.Definition): List[quotes.reflect.Definition] =
          import quotes.reflect.*
          tree match
            case ClassDef(name, constr, parents, selfOpt, body) =>
    
              val tpe = TypeRepr.of[TC].appliedTo(List(tree.symbol.companionClass.typeRef))
    
              val tcSym = Symbol.newVal(tree.symbol, "tc", tpe, Flags.Given, Symbol.noSymbol)
              val tcDef = tpe.asType match
                case '[TC[t]] => ValDef(tcSym, Some('{ new TC[t] }.asTerm))
    
              val res = List(ClassDef.copy(tree)(name, constr, parents, selfOpt, body :+ tcDef))
              println(res.map(_.show))
              res
            case _ =>
              report.errorAndAbort("@modifyObj can annotate only classes")
    
    import Macros.{TC, genObj, modifyObj}
    import scala.annotation.experimental
    
    @experimental
    object App:
      @genObj
      class A
    
    //scalac: List(@Macros.genObj class A(),
    //lazy val A: App.A.type = new App.A(),
    //object A extends java.lang.Object { this: App.A.type =>
    //  val tc: Macros.TC[App.A] = new Macros.TC[App.A]()
    //})
    
    import Macros.{TC, genObj, modifyObj}
    import scala.annotation.experimental
    
    @experimental
    object App:
      class A
    
      @modifyObj
      object A
    
    //scalac: List(@Macros.modifyObj object A {
    //  val tc: Macros.TC[App.A] = new Macros.TC[App.A]()
    //})
    

    Macro Annotations in Scala 3

    How to generate parameterless constructor at compile time using scala 3 macro?

    Scala 3 macro to create enum