Search code examples
scalametaprogrammingscala-macrosscala-3

How can I create an enum value dynamically in Scala 3 macros?


I'm creating an enum value dynamically. I have a type of the enum (not the object itself) and a string representing a valid value, e.g. "Red" for Color.Red.

// T is Color (enum).  I've discovered the valueOf() method lives in the companion object
val sym = TypeRepr.of[T].classSymbol.get.companionClass

// Try to invoke the valueOf() method on the enum with the value, enumValue ("Red")
Apply(Select.unique(New(TypeIdent(sym)), "valueOf"), List('{ enumValue }.asTerm)).asExprOf[T]

It compiles but is deeply unhappy when run. Doesn't like New there, which makes sense because we don't "new" objects.

What's the proper way to invoke valueOf dynamically in a macro?


Solution

  • enum Test:
      case A
    
    object TestMacros: {
      def printAST(using quotes: scala.quoted.Quotes): Unit = {
        import quotes.reflect.*
        println('{ Test.valueOf("A") }.asTerm.show(using Printer.TreeStructure))
      }
    }
    
    // call printAST insome inline def
    

    prints:

    Inlined(Some(TypeIdent("TestMacros$")), Nil, Apply(Select(Ident("Test"), "valueOf"), List(Literal(StringConstant("A")))))
    

    after removing Inline garbadge:

    Apply(Select(Ident("Test"), "valueOf"), List(Literal(StringConstant("A"))))
    

    To construct this tree (your problem seem to be Ident) I guess we can use:

    val sym = TypeRepr.of[Test].typeSymbol
    Apply(Select.unique(Ref(sym), "valueOf"), List(...))
    //                  ^ Ref can be used to turn a symbol of singleton type to Term
    

    Usage of Ref is the same whether you construct anything dynamically or in compile time.

    When I need to handle sealed hierarchies "statically" I use the following apprach:

    1. obtain a list of all children types
    def extractRecursively(sym: Symbol): List[Symbol] =
      if sym.flags.is(Flags.Sealed) then sym.children.flatMap(extractRecursively)
      else if sym.flags.is(Flags.Enum) then List(sym.typeRef.typeSymbol)
      else if sym.flags.is(Flags.Module) then List(sym.typeRef.typeSymbol.moduleClass)
      else List(sym)
    
    extractRecursively(TypeRepr.of[A].typeSymbol).distinct.map { subtype =>
      subtype.primaryConstructor.paramSymss match {
        // subtype takes type parameters
        case typeParamSymbols :: _ if typeParamSymbols.exists(_.isType) =>
          // we have to figure how subtypes type params map to parent type params
          val appliedTypeByParam: Map[String, TypeRepr] =
            subtype.typeRef
              .baseType(TypeRepr.of[A].typeSymbol)
              .typeArgs
              .map(_.typeSymbol.name)
              .zip(TypeRepr.of[A].typeArgs)
              .toMap
             
          val typeParamReprs: List[TypeRepr] = typeParamSymbols.map(_.name).map(appliedTypeByParam)
          subtype.typeRef.appliedTo(typeParamReprs).asType
        // subtype is monomorphic
        case _ =>
          subtype.typeRef.asType
    }
    
    1. turn Types into Exprs
    // subtype: Type[?]
    //
    // put the subtype into implicit scope with some name, e.g.:
    //
    // type B <: A
    // given B: Type[B] = subtype.asInstanecOf[Type[B]]
    
    val B = TypeRepr.of[B] // type of A's subtype
    val sym = B.typeSymbol
    
    // case object B extends A
    def isScala2Enum = sym.flags.is(Flags.Case | Flags.Module)
    // Scala 3 enum's parametersless case is NOT case object
    // but: val B = new A {}
    def isScala3Enum = sym.flags.is(Flags.Case | Flags.Enum | Flags.JavaStatic)
    // Java: enum A { B; }
    def isJavaEnumValue: Boolean = TypeRepr.of[B] <:< TypeRepr.of[java.lang.Enum[?]] && !sym.isAbstract
    
    if (isScala3Enum || isJavaEnumValue) then Some(Ref(sym).asExprOf[B])
    else if isScala2Enum then Some(Ref(sym.companionModule).asExprOf[B])
    else None
    
    1. handle cases when some subtypes are case classes (create None for the code above)

    (Actually, Scala 3 macros treat Java enums the same as Scala 3 enums without any parenthesis, so || isJavaEnumValue is kinda redundant - you could remove it and it should still works with Scala 2's sealed hierarchy of case objects, Scala 3's enums AND Java enums).

    As you can see in both cases, dynamical and statical, when I need to access case object/parameterless case in Scala 3's enum/Java's enum/companion object's method I am using Ref <: Term to turn Symbol into Term (and it will be Ident internally).