Matching XML Literals in Scala Macros
Asked Answered
S

2

8

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?

Stockholm answered 21/3, 2014 at 12:31 Comment(0)
M
2

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
    }
  }
Mimi answered 2/4, 2014 at 7:46 Comment(2)
Thanks a lot for your answer! I will not have the time to try your code today but I will try to give you some feedback tomorrow.Stockholm
Ok, that did work. Don't really know why it didn't work when I tried it before. Must have made some stupid mistake. Thank you very much for your effort and for lodging the issue!Stockholm
R
2

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.

Recha answered 9/4, 2014 at 21:13 Comment(2)
You guys are the Agents of SHIELD for ASTs. I looked at Unliftable for the 1st time when answering above, but I didn't understand point 2: "We only define Unliftable for runtime universe, it won’t be available in macros."Mimi
Liftable is path dependent over universe. To use it you need a universe to import it from and when you define it for that universe it won't work for all universes, just for that one. To abstract over it you can put universe into trait and instantiate the trait for each universe you interested in (typically runtime and macros). e.g.: split into Liftables vs RuntimeLiftables vs MacroLiftables in scalaxml/macros.Recha

© 2022 - 2024 — McMap. All rights reserved.