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
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:
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.