Search code examples
scalametaprogrammingscala-macrosscala-3

scala 3 macro: get class properties


i want to writing a macro to get property names of a class. but can not use Symbol module in quoted statement. i receive blow error...

inline def getProps(inline className: String): Iterable[String] = ${ getPropsImpl('className) }
private def getPropsImpl(className: Expr[String])(using Quotes): Expr[Iterable[String]] = {
  import quotes.reflect.*

  val props = '{
    Symbol.classSymbol($className).fieldMembers.map(_.name) // error access to parameter x$2 from 
  }                                                            wrong staging level:
  props                                                        - the definition is at level 0,
}                                                              - but the access is at level 1.

Solution

  • There are compile time and runtime of macros. And there are compile time and runtime of main code. The runtime of macros is the compile time of main code.

    def getPropsImpl... = 
      '{ Symbol.classSymbol($className).fieldMembers.map(_.name) }
      ...
    

    is incorrect because what Scala 3 macros do is transforming trees into trees (i.e. Exprs into Exprs, Expr is a wrapper over a tree) (*). The tree

    Symbol.classSymbol($className).fieldMembers.map(_.name)
    

    will make no sense inside the scope of application site. Symbol, Symbol.classSymbol etc. make sense here, inside the scope of macro.

    def getPropsImpl... = 
      Symbol.classSymbol(className).fieldMembers.map(_.name)
      ...
    

    would be also incorrect because className as a value doesn't exist yet, it's just a tree now.

    I guess correct is with .valueOrAbort

    import scala.quoted.*
    
    inline def getProps(inline className: String): Iterable[String] = ${getPropsImpl('className)}
    
    def getPropsImpl(className: Expr[String])(using Quotes): Expr[Iterable[String]] = {
      import quotes.reflect.*
    
      Expr.ofSeq(
        Symbol.classSymbol(className.valueOrAbort).fieldMembers.map(s =>
          Literal(StringConstant(s.name)).asExprOf[String]
        )
      )
    }
    

    Usage:

    // in other file
    getProps("mypackage.App.A") //ArraySeq(s, i)
    
    // in other subproject
    package mypackage
    object App {
      case class A(i: Int, s: String)
    }
    

    (*) Scala 2 macros can do more with c.eval. In Scala 3 there is similar thing staging.run but it's forbidden in macros.


    Actually, c.eval (or forbidden staging.run) can be emulated in Scala 3 too

    get annotations from class in scala 3 macros