Search code examples
scalaenumsmacrosscala-macrosscala-3

Scala 3 macro to create enum


I'm wondering if it is possible to write a macro in Scala 3 that take a set of strings and turn it into an enum type with with those strings as cases?

For example, I would like to write a class with an internal type generated from the input element:

import scala.quoted.*

class Example(myEnumElements:Seq[String]) {
  
  inline def buildEnum(inline elts:Seq[String]): Unit = ${ buildEnumType('elts) }

  def buildEnumType(e: Expr[Seq[String]])(using Quotes, Type[Seq]): Expr[Unit] = '{
    enum MyEnum:
      ???
  }
}
...
// Possibly in another file?

val example = Example(Seq("A","B","C"))

def someConvenienceFunction(e:example.MyEnum) = e match
  case A => "apple"
  case B => "banana"
  case C => "cranberry"
...
// Possibly in another file?

someConvenienceFunction(example.A)  // "apple"
someConvenienceFunction(example.D)  // compile error

Solution

  • Scala 3 macros are currently only def macros. They are not for generating classes, enums etc. Even if you define an enum inside buildEnumType it will be visible only inside the block {...} that buildEnum call expands into.

    Try to use code generation instead.

    How to generate a class in Dotty with macro?

    https://users.scala-lang.org/t/macro-annotations-replacement-in-scala-3/7374

    How to create variables with macros in Scala (Scala 2)

    Resolving variables in scope modified by Scala macro (Scala 2)


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

    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.

    The macro annotation generating "enum" (a seled trait and case objects extending the trait) should be the following:

    build.sbt

    scalaVersion := "3.3.0-RC3"
    
    import scala.annotation.{MacroAnnotation, experimental}
    import scala.quoted.*
    
    object Macros {
      @experimental
      class buildEnum extends MacroAnnotation:
        def transform(using Quotes)(tree: quotes.reflect.Definition): List[quotes.reflect.Definition] =
          import quotes.reflect.*
    
          extension (symb: Symbol)
            def setFlags(flags: Flags): Symbol =
              given dotty.tools.dotc.core.Contexts.Context =
                quotes.asInstanceOf[scala.quoted.runtime.impl.QuotesImpl].ctx
              symb.asInstanceOf[dotty.tools.dotc.core.Symbols.Symbol]
                .denot.setFlag(flags.asInstanceOf[dotty.tools.dotc.core.Flags.FlagSet])
              symb
    
          tree match
            case ClassDef(name, constr, parents, selfOpt, body) =>
              val parents = List(TypeTree.of[Any])
              val cls = Symbol.newClass(tree.symbol, "MyEnum", parents.map(_.tpe), decls = _ => Nil, selfType = None)
                .setFlags(Flags.Trait | Flags.Sealed)
              val clsDef = ClassDef(cls, parents, body = Nil)
    
              def mkObject(name: String): (ValDef, ClassDef) =
                val modParents = List(TypeTree.of[Any], Inferred(cls.typeRef))
                val mod = Symbol.newModule(tree.symbol, name, modFlags = Flags.EmptyFlags, clsFlags = Flags.EmptyFlags,
                  modParents.map(_.tpe), decls = _ => Nil, privateWithin = Symbol.noSymbol)
                  .setFlags(Flags.Case)
                //val modCls = mod.moduleClass
                ClassDef.module(mod, modParents, body = Nil)
    
              val enumTrees = clsDef :: List("A", "B", "C").map(mkObject(_).toList).reduce(_ ++ _)
    
              val res = List(ClassDef.copy(tree)(name, constr, parents, selfOpt, body ++ enumTrees))
              println(res.map(_.show))
              res
    
            case _ =>
              report.errorAndAbort("@modifyObj can annotate only classes")
    
    import Macros.buildEnum
    import scala.quoted.*
    import scala.annotation.experimental
    
    object App:
      @buildEnum @experimental
      class Example(myEnumElements:Seq[String])
    
    //scalac: List(@scala.annotation.experimental @Macros6.buildEnum class Example(myEnumElements: scala.Seq[scala.Predef.String]) extends scala.Any {
    //  sealed trait MyEnum extends scala.Any
    //  object A extends scala.Any with Example.this.MyEnum { this: Example.this.A.type =>
    //  }
    //  object B extends scala.Any with Example.this.MyEnum { this: Example.this.B.type =>
    //  }
    //  object C extends scala.Any with Example.this.MyEnum { this: Example.this.C.type =>
    //  }
    //})
    

    Macro Annotations in Scala 3

    How to generate a class in Dotty with macro?

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

    Method Override with Scala 3 Macros