String interpolation and macro: how to get the StringContext and expression locations

前端 未结 1 764
耶瑟儿~
耶瑟儿~ 2021-02-13 09:21

I\'m trying to implement a custom string interpolation method with a macro and I need some guidance on using the API.

Here is what I want to do:

/** expe         


        
相关标签:
1条回答
  • 2021-02-13 09:30

    I found a runnable solution after some hours of hard work:

    object Macros {
    
      import scala.reflect.macros.Context
      import language.experimental.macros
    
      sealed trait Piece
      case class Place(str: String) extends Piece
      case class Name(str: String) extends Piece
      case class Pos(column: Int, line: Int)
      case class LocatedPieces(located: List[(String, Piece, Pos)])
    
      implicit class s2pieces(sc: StringContext) {
        def s2(pieces: Piece*) = macro s2impl
      }
    
      // pieces contain all the Piece instances passed inside of the string interpolation
      def s2impl(c: Context)(pieces: c.Expr[Piece]*): c.Expr[LocatedPieces] = {
        import c.universe.{ Name => _, _ }
    
        c.prefix.tree match {
          // access data of string interpolation
          case Apply(_, List(Apply(_, rawParts))) =>
    
            // helper methods
            def typeIdent[A : TypeTag] =
              Ident(typeTag[A].tpe.typeSymbol)
    
            def companionIdent[A : TypeTag] =
              Ident(typeTag[A].tpe.typeSymbol.companionSymbol)
    
            def identFromString(tpt: String) =
              Ident(c.mirror.staticModule(tpt))
    
            // We need to translate the data calculated inside of the macro to an AST
            // in order to write it back to the compiler.
            def toAST(any: Any) =
              Literal(Constant(any))
    
            def toPosAST(column: Tree, line: Tree) =
              Apply(
                Select(companionIdent[Pos], newTermName("apply")),
                List(column, line))
    
            def toTupleAST(t1: Tree, t2: Tree, t3: Tree) =
              Apply(
                TypeApply(
                  Select(identFromString("scala.Tuple3"), newTermName("apply")),
                  List(typeIdent[String], typeIdent[Piece], typeIdent[Pos])),
                List(t1, t2, t3))
    
            def toLocatedPiecesAST(located: Tree) =
              Apply(
                Select(companionIdent[LocatedPieces], newTermName("apply")),
                List(located))
    
            def toListAST(xs: List[Tree]) =
              Apply(
                TypeApply(
                  Select(identFromString("scala.collection.immutable.List"), newTermName("apply")),
                  List(AppliedTypeTree(
                    typeIdent[Tuple3[String, Piece, Pos]],
                    List(typeIdent[String], typeIdent[Piece], typeIdent[Pos])))),
                xs)
    
            // `parts` contain the strings a string interpolation is built of
            val parts = rawParts map { case Literal(Constant(const: String)) => const }
            // translate compiler positions to a data structure that can live outside of the compiler
            val positions = pieces.toList map (_.tree.pos) map (p => Pos(p.column, p.line))
            // discard last element of parts, `transpose` does not work otherwise
            // trim parts to discard unnecessary white space
            val data = List(parts.init map (_.trim), pieces.toList, positions).transpose
            // create an AST containing a List[(String, Piece, Pos)]
            val tupleAST = data map { case List(part: String, piece: c.Expr[_], Pos(column, line)) =>
              toTupleAST(toAST(part), piece.tree, toPosAST(toAST(column), toAST(line)))
            }
            // create an AST of `LocatedPieces`
            val locatedPiecesAST = toLocatedPiecesAST(toListAST(tupleAST))
            c.Expr(locatedPiecesAST)
    
          case _ =>
            c.abort(c.enclosingPosition, "invalid")
        }
      }
    }
    

    Usage:

    object StringContextTest {
      val place: Piece = Place("world")
      val name: Piece = Name("Eric")
      val pieces = s2"""
        Hello $place
        How are you, $name?
      """
      pieces.located foreach println
    }
    

    Result:

    (Hello,Place(world),Pos(12,9))
    (How are you,,Name(Eric),Pos(19,10))
    

    I didn't thought that it can take so many time to get all things together, but it was a nice time of fun. I hope the code conforms your requirements. If you need more information on how specific things are working then look at other questions and their answers on SO:

    • Information on how ASTs are constructed
    • How to work with TypeTag
    • How to use reify in the REPL to get information about the AST

    Many thanks to Travis Brown (see comments), I got a far shorter solution to compile:

    object Macros {
    
      import scala.reflect.macros.Context
      import language.experimental.macros
    
      sealed trait Piece
      case class Place(str: String) extends Piece
      case class Name(str: String) extends Piece
      case class Pos(column: Int, line: Int)
      case class LocatedPieces(located: Seq[(String, Piece, Pos)])
    
      implicit class s2pieces(sc: StringContext) {
        def s2(pieces: Piece*) = macro s2impl
      }
    
      def s2impl(c: Context)(pieces: c.Expr[Piece]*): c.Expr[LocatedPieces] = {
        import c.universe.{ Name => _, _ }
    
        def toAST[A : TypeTag](xs: Tree*): Tree =
          Apply(
            Select(Ident(typeOf[A].typeSymbol.companionSymbol), newTermName("apply")),
            xs.toList)
    
        val parts = c.prefix.tree match {
          case Apply(_, List(Apply(_, rawParts))) =>
            rawParts zip (pieces map (_.tree)) map {
              case (Literal(Constant(rawPart: String)), piece) =>
                val line = c.literal(piece.pos.line).tree
                val column = c.literal(piece.pos.column).tree
                val part = c.literal(rawPart.trim).tree
                toAST[(_, _, _)](part, piece, toAST[Pos](line, column))
          }
        }
        c.Expr(toAST[LocatedPieces](toAST[Seq[_]](parts: _*)))
      }
    }
    

    It abstracts over the verbose AST construction and its logic is a little different but nearly the same. If you have difficulties in understanding how the code works, first try to understand the first solution. It is more explicit in what it does.

    0 讨论(0)
提交回复
热议问题