15

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:

/** expected
  * LocatedPieces(List(("\nHello ", Place("world"), Position()), 
                       ("\nHow are you, ", Name("Eric"), Position(...)))
  */
val locatedPieces: LocatedPieces = 
  s2"""
    Hello $place

    How are you, $name
    """

val place: Piece = Place("world")
val name: Piece = Name("Eric")

trait Piece
case class Place(p: String) extends Piece
case class Name(n: String) extends Piece

/** sequence of each interpolated Piece object with:
  * the preceding text and its location
  */  
case class LocatedPieces(located: Seq[(String, Piece, Position)]) 

implicit class s2pieces(sc: StringContext) {
  def s2(parts: Piece*) = macro s2Impl
}

def impl(c: Context)(pieces: c.Expr[Piece]*): c.Expr[LocatedPieces] = {
  // I want to build a LocatedPieces object with the positions for all 
  // the pieces + the pieces + the (sc: StringContext).parts
  // with the method createLocatedPieces below
  // ???     
} 

def createLocatedPieces(parts: Seq[String], pieces: Seq[Piece], positions: Seq[Position]):
  LocatedPieces = 
  // zip the text parts, pieces and positions together to create a LocatedPieces object
  ???

My questions are:

  1. How do I access the StringContext object inside the macro in order to get all the StringContext.parts strings?

  2. How can I grab the positions of a each piece?

  3. How can I call the createLocatedPieces method above and reify the result to get the result of the macro call?

kiritsuku
  • 52,967
  • 18
  • 114
  • 136
Eric
  • 15,494
  • 38
  • 61
  • I have tried various pieces of the API but I haven't been able yet to assemble the full solution. Any advice or general direction would help. And a complete solution would get my eternal gratitude :-) – Eric Mar 10 '13 at 23:29
  • I'm not sure if it has the answer, but your question reminded me of this post: http://hootenannylas.blogspot.com.au/2013/02/syntax-checking-in-scala-string.html – Kristian Domagala Mar 11 '13 at 02:38
  • I read it already and my use case does slightly more. But I know Tony and I'll ask him to help me during the next ScalaSyd this week if I don't get an answer in the meantime :-). – Eric Mar 11 '13 at 03:25
  • Hi - could you give an example with maybe more than one Piece? What is the input, what should the output look like. I can only guess and would probably be wrong. My advice - have a look at some [scala macros](https://github.com/search?q=scala+macros) at github. – michael_s Mar 11 '13 at 05:16
  • I updated the example and I'm going to have a look at the github projects, it's a good suggestion. – Eric Mar 11 '13 at 06:05
  • What do you mean by position? The x and y position of the symbol in the source file? – kiritsuku Mar 11 '13 at 13:42
  • Are you willing to use some other representation of the position (e.g. a `(line, column)` coordinate pair)? Smuggling the `piece.tree.pos` out of the macro would be the trickiest part. – Travis Brown Mar 11 '13 at 14:13
  • (line, column) is indeed what I'm after. – Eric Mar 11 '13 at 20:20

1 Answers1

11

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.

Community
  • 1
  • 1
kiritsuku
  • 52,967
  • 18
  • 114
  • 136
  • 3
    +1, and I don't mean to steal your thunder, but it's possible to do this [_much_ more concisely](https://gist.github.com/travisbrown/5136824). I started this implementation this morning but didn't post it because (like yours) it doesn't return a full `Position`. – Travis Brown Mar 11 '13 at 19:17
  • Thanks for your hard work! I'm going to adapt your solution to my exact use case but it looks like I have all the necessary information here about how to extract the raw text, the positions and package the whole up. Travis, I'm also going to have a look at your gist which, at first glance, has some intriguing underscores in some positions. – Eric Mar 11 '13 at 20:23
  • 1
    @TravisBrown: So many thanks, your solution is awesome. I *knew* there must be a way to abstract over all this AST construction shit, but I didn't came up with the solution. Thinking about multiple parameters and tuples just as a list of parameters is damn cool. My second try is so far shorter now. – kiritsuku Mar 11 '13 at 20:45
  • 1
    @Eric: I already combined Travis' solution with mine. The result is impressive. – kiritsuku Mar 11 '13 at 20:47
  • Note that it's possible to make this a little more concise [with `reify` and `splice`](https://gist.github.com/travisbrown/5136824#comment-795998), but see also my comments about why you may not want to. – Travis Brown Mar 11 '13 at 21:45