Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Matching XML Literals in Scala Macros

I want to transform Scala XML literals with a macro. (Not a string literal with XML but actual XML literals). As far as I understand, XML literals are not actually built into the language on the AST level but are desugared in the parser. Interestingly though, this does work:

case q"<specificTag></specificTag>" => ... // succeeds for specificTag with no
                                           // attributes and children

But obviously, this is totally useless because it is impossible to match arbitrary xml that way. Something like

case q"<$prefix:$label ..$attrs>$children</$prefix:$label>" => ...

can not work because we would have to bind the same variable twice in a pattern.

Printing out the tree of such an xml literal expression actually gives the desugared version. For example.

new _root_.scala.xml.Elem(null,"specificTag",_root_.scala.xml.Null,$scope,false)

But trying to match this fails:

case q"new _root_.scala.xml.Elem(..$params)" => ... // never succeeds

I am confused! My question is: Is there a way to reliably match arbitrary xml litarals in scala macros? Additionally: Why are they supported in quasiquotes for constant xml and not for the desugared value after all?

like image 869
Martin Ring Avatar asked Mar 21 '14 12:03

Martin Ring


2 Answers

The xml is wrapped in blocks, macro invoked as rename( <top><bottom>hello</bottom></top> ). I noticed that by looking at the incoming tree, not what's constructed by quasiquotes.

I had lodged this issue when I looked at your question previously; I don't know whether my SO is that; I tried bumping SS in sbt. There's another SO issue that's probably unrelated.

  class Normalizer(val c: Context) {
    import c.universe._ 
    def impl(e: c.Tree) = e match {
      case Block(List(), Block(List(), x)) => x match {
        case q"new scala.xml.Elem($prefix, $label, $attrs, $scope, $min, $t)" =>
          Console println s"Childed tree is ${showRaw(e)}" 
          val b = t match {
            case Typed(b, z) => c.untypecheck(b.duplicate)
            case _           => EmptyTree
          } 
          val Literal(Constant(tag: String)) = label
          val x = c.eval(c.Expr[NodeBuffer](b))
          //q"""<${tag.reverse}>..$x</${tag.reverse}>"""  // SO
          e
        case q"new scala.xml.Elem($prefix, $label, $attrs, $scope, $min)" =>
          Console println s"Childless tree is ${showRaw(e)}" ; e
        case _ => Console println s"Tree is ${showRaw(e)}" ; e
      }
      case _ => Console println s"Nonblock is ${showRaw(e)}" ; e
    }
  }
like image 164
som-snytt Avatar answered Nov 08 '22 01:11

som-snytt


Unfortunately quasiquotes don't natively support matching of xml literals and until today the only way to do it was to match on desugared tree as demonstrated by @som-snytt. But it's very easy to get it wrong and such manipulations may require so many AST nodes that they will blow up the pattern matcher.

To address this weakness we've just released a first milestone of scalamacros/xml, a library that turns this problem around: instead of working with AST of XML it lets you work with pure XML nodes instead:

scala> val q"${elem: xml.Elem}" = q"<foo><bar/></foo>"
elem: scala.xml.Elem = <foo><bar/></foo>

Here we use unlifting to convert code to value and than we can just process it as xml. In the end after processing you will probably want to convert it back to AST through lifting):

scala> q"$elem"
res4: org.scalamacros.xml.RuntimeLiftables.__universe.Tree =
new _root_.scala.xml.Elem(null, "foo", _root_.scala.xml.Null, $scope, false, ({
  val $buf = new _root_.scala.xml.NodeBuffer();
  $buf.$amp$plus(new _root_.scala.xml.Elem(null, "bar", _root_.scala.xml.Null, $scope, true));
  $buf
}: _*))

In case your original AST case some code snippets they will be converted to special Unquote node that contains such snippets:

scala> val q"${elem: xml.Elem}" = q"<foo>{x + y}</foo>"
elem: scala.xml.Elem = <foo>{x.+(y)}</foo>

scala> val <foo>{Unquote(q"x + y")}</foo> = elem
// matches 

It's also easy to filter all unquote nodes through projection:

scala> elem \ "#UNQUOTE"
res6: scala.xml.NodeSeq = NodeSeq({x.+(y)})

You might also be interested in checking out example sbt project with simple macro that uses this library or have a deeper look at our test suite.

like image 26
Denys Shabalin Avatar answered Nov 07 '22 23:11

Denys Shabalin