How to generate a class in Dotty with macro?
Asked Answered
G

2

10

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

Zlaja

Glycoside answered 24/12, 2019 at 21:26 Comment(4)
Macros were never meant for creating totally new types (classes including). At best you could create a macro-generated instance of a class tuned for some arguments. I think macro annotations and/or compiler plugins could be used to insert new class body into companion object of annotated class (not a good idea), but that's it. If you want to generate classes use a code generator instead.Psittacine
For @main annotation in Dotty I find only class: main extends scala.annotation.Annotation {} in source code. So, does macro annotation need compiler plugin to work?Glycoside
@Glycoside @main is not a macro annotation, it's individual annotation managed by Dotty compiler. Currently you can't create such custom annotation without compiler plugin to Dotty.Engrail
@Glycoside I updated my answer with examples of macro annotations to be added in Scala 3.3.0Engrail
E
10

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

Engrail answered 25/12, 2019 at 1:46 Comment(5)
But Scalameta does not (yet?) support Scala3?Paternoster
@Paternoster Scalameta does support Scala 3. You can use Scalameta in order to read/analyze/transform/generate Scala 3 code scastie.scala-lang.org/DmytroMitin/T0tFbxxgROGn0lUbHMbUBQ/1 Scalameta just runs in Scala 2.Engrail
@Paternoster Actually, Scalameta not only handles Scala 3 code, it even runs in Scala 3 (because of Scala 2 - Scala 3 interop). You just can't use Scala 2 macros e.g. q"..." interpolator scastie.scala-lang.org/DmytroMitin/XZHWRGUiRYmpuSnKB1rrqQ libraryDependencies += "org.scalameta" %% "scalameta" % "4.5.13" cross CrossVersion.for3Use2_13Engrail
contributors.scala-lang.org/t/pre-sip-export-macros/6168 github.com/littlenag/dotty/tree/export-macro gist.github.com/littlenag/d0c9dfddeb9002684c6effef18c2ec5eEngrail
WIth the @experimental annotation, we can hardly use this in library code. It make the whole code base become a experimental scope. Is there some workaround?Sic
E
7

You can make a transparent macro that returns a structural type to generate types with any vals and defs that you wish.

Here is an example; method props, when called with a Product type, creates an object with the first Product element name as a String val.

case class User(firstName: String, age: Int)

// has the type of Props { val firstName: String }
val userProps = props[User]

println(userProps.firstName) // prints "prop for firstName"
println(userProps.lastName) // compile error

And the implementation, which is a little tricky but not too bad:

import scala.compiletime.*
import scala.quoted.*
import scala.deriving.Mirror

class Props extends Selectable:
  def selectDynamic(name: String): Any =
    "prop for " + name

transparent inline def props[T] =
  ${ propsImpl[T] }

private def propsImpl[T: Type](using Quotes): Expr[Any] =
  import quotes.reflect.*

  Expr.summon[Mirror.ProductOf[T]].get match
    case '{ $m: Mirror.ProductOf[T] {type MirroredElemLabels = mels; type MirroredElemTypes = mets } } =>
      Type.of[mels] match
        case '[mel *: melTail] =>
          val label = Type.valueOfConstant[mel].get.toString

          Refinement(TypeRepr.of[Props], label, TypeRepr.of[String]).asType match
            case '[tpe] =>
              val res = '{
                val p = Props()
                p.asInstanceOf[tpe]
              }
              println(res.show)
              res

Using recursion, you can refine the Refinement (since Refinement <: TypeRepr) until everything is built out.

With that said, using transparent inline or even Scala 2 macro annotations to generate new types makes it very hard for IDEs to support auto-completion. So if possible, I recommend using the standard Typeclass derivation.

You can even derive default behavior for standard traits:

trait SpringDataRepository[E, Id]:
  def findAll(): Seq[E]

trait DerivedSpringDataRepository[E: Mirror.ProductOf, Id]:
  def findAll(): Seq[E] = findAllDefault[E, Id]()
  
private inline def findAllDefault[E, Id](using m: Mirror.ProductOf[E]): Seq[E] =
  findAllDefaultImpl[E, m.MirroredLabel, m.MirroredElemLabels]()

private inline def findAllDefaultImpl[E, Ml, Mels](columns: ArrayBuffer[String] = ArrayBuffer()): Seq[E] =
  inline erasedValue[Mels] match
    case _: EmptyTuple =>
      // base case
      println("executing.. select " + columns.mkString(", ") + " from " + constValue[Ml])
      Seq.empty[E]
    case _: (mel *: melTail) =>
      findAllDefaultImpl[E, Ml, melTail](columns += constValue[mel].toString) 
      

Then, all a user has to do is extend DerivedSpringDataRepository with their Product type:

case class User(id: Int, first: String, last: String)

class UserRepo extends DerivedSpringDataRepository[User, Int]

val userRepo = UserRepo()

userRepo.findAll() // prints "executing.. select id, first, last from User"
Earlie answered 18/4, 2021 at 23:2 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.