使用Scala宏操作变量声明

时间:2019-03-08 18:23:27

标签: scala scala-macros

我想使用scala(v2.12.8)宏来操纵给定块的所有变量声明。在此示例中,添加值23。

例如:

val myblock = mymanipulator {
    var x = 1
    x = 4
    var y = 1
    x + y
  }
print( myblock )

成为

{
  var x = (1).+(23);
  x = 4;
  var y = (1).+(23);
  x.+(y)
}

为此,我实现了mymanipulator,如下所示:

import scala.language.experimental.macros
import scala.reflect.macros.blackbox.Context
import scala.language.implicitConversions

object mymanipulator {
  def apply[T](x: => T): T = macro impl

  def impl(c: Context)(x: c.Tree) = { import c.universe._

    val q"..$stats" = x
    val loggedStats = stats.flatMap { stat =>

      stat match {
        case ValDef(mods, sym, tpt, rhs) => {
          List( q"var $sym : $tpt = $rhs + 23" )
        }

        case _ => {
          List( stat )
        }
      }

    }

    val combined = q"..$loggedStats"

    c.info(c.enclosingPosition, "combined: " + showRaw(combined), true)

    combined
  }
}

我在编译宏的过程中得到了以下信息:

Information:(21, 31) combined: {
  var x = (1).+(23);
  x = 4;
  var y = (1).+(23);
  x.+(y)
}
  val myblock = mymanipulator {

但是当我使用上面给定的代码块执行mymanipulator时,我得到以下错误消息:

Error:scalac: Error while emitting Test.scala
variable y

当我将实现更改为不执行任何操作时,也会发生此错误:

 stat match {
        case ValDef(mods, sym, tpt, rhs) => {
          List( q"var $sym : $tpt = $rhs" )
        }

        case _ => {
          List( stat )
        }
      }

仅当我返回stat时错误才会消失

 stat match {
    case ValDef(mods, sym, tpt, rhs) => {
       List( stat )
     }

     case _ => {
        List( stat ) 
      }
  }

有人可以告诉我我做错了吗?谢谢

2 个答案:

答案 0 :(得分:1)

您应该在转换之前取消对树的类型检查

  object mymanipulator {
    def apply[T](x: => T): T = macro impl

    def impl(c: blackbox.Context)(x: c.Tree): c.Tree = {
      import c.universe._

      val q"..$stats" = c.untypecheck(x) // here
      val loggedStats = stats.flatMap { stat =>

        stat match {
          case ValDef(mods, sym, tpt, rhs) /*q"$mods var $sym : $tpt = $rhs"*/ => {
            List( q"$mods var $sym : $tpt = $rhs + 23" )
          }

          case _ => {
            List( stat )
          }
        }

      }

      val combined = q"..$loggedStats"

      c.info(c.enclosingPosition, "combined: " + showRaw(combined), true)

      combined
    }
  }

Macro untypecheck required

What is wrong with this def macro?

答案 1 :(得分:1)

取消类型检查源树会删除子树的类型。 这使得无法根据表达式类型进行树操作。 那么如何使用类型检查树并替换宏源代码中的术语定义?

如果我们替换术语的定义(或者甚至简单地重新组合相同的术语定义,实际上有新的树),那么编译器在识别该术语时会失败,并出现如下错误:

Could not find proxy for val <val-name>

REPL 错误就像

Error while emitting <console>
variable j

对结果树进行简单的取消类型检查或什至进一步的类型检查都无济于事。 我在不同的答案中发现了几个原因:

  1. 旧的(从源树中重用)Ident 仍然指的是它的旧符号定义,该定义在结果树中已经不存在(我们替换了它)
  2. 重用的未更改符号定义(其树)已更改其所有者(代码已包装或重新包装)

帮助我的解决方案是重新创建所有引用本地(源代码)定义的 Ident。引用外部符号定义的标识符应保持不变(如“scala”、内部引用的外部类型等),否则编译将失败。

以下示例(可在 REPL 模式下在 IDEA Worksheet 中运行,在 2.12 中尝试)显示了使用 Transformer 重新创建仅引用本地定义的 Ident。新替换的 Ident 现在不会引用旧定义。

它使用的语法树涵盖了实现目标所需的唯一 Scala 语法。这个语法所不知道的一切都变成了保存原始源代码子树的OtherTree。

import scala.reflect.macros.blackbox
import scala.language.experimental.macros

trait SyntaxTree {
  val c: blackbox.Context
  import c.universe._

  sealed trait Expression

  case class SimpleVal(termName: TermName, expression: Expression) extends Expression
  case class StatementsBlock(tpe: Type, statements: Seq[Expression], expression: Expression) extends Expression
  case class OtherTree(tpe: Type, tree: Tree) extends Expression

  object Expression {
    implicit val expressionUnliftable: Unliftable[Expression] = Unliftable[Expression] (({
      case q"val ${termName: TermName} = ${expression: Expression}" =>
        SimpleVal(termName, expression)
      case tree@Block(_, _) => // matching on block quosiquotes directly produces StackOverflow in this Syntax: on the single OtherTree node
        val q"{ ..${statements: Seq[Expression]}; ${expression: Expression} }" = tree
        StatementsBlock(tree.tpe, statements, expression)
      case otherTree =>
        OtherTree(otherTree.tpe, otherTree)
    }: PartialFunction[Tree, Expression]).andThen(e => {println("Unlifted", e); e}))

    implicit val expressionLiftable: Liftable[Expression] = Liftable[Expression] {
      case SimpleVal(termName, expression) =>
        q"val $termName = $expression + 23"
      case StatementsBlock(_, statements, expression) =>
        q"{ ..${statements: Seq[Expression]}; ${expression: Expression} }"
      case OtherTree(_, otherTree) =>
        c.untypecheck(otherTree) // untypecheck here or before final emitting of the resulting Tree: fun, but in my complex syntax tree this dilemma has 0,01% tests impact (in both cases different complex tests fails in ToolBox)
    }
  }
}

class ValMacro(val c: blackbox.Context) extends SyntaxTree {
  import c.universe._

  def valMacroImpl(doTransform: c.Expr[Boolean], doInitialUntypecheck: c.Expr[Boolean])(inputCode: c.Expr[Any]): c.Tree = {
    val shouldDoTransform = doTransform.tree.toString == "true"
    val shouldUntypecheckInput = doInitialUntypecheck.tree.toString == "true"

    val inputTree = if (shouldUntypecheckInput)
      c.untypecheck(inputCode.tree) // initial untypecheck helps but we loose parsed expression types for analyses
    else
      inputCode.tree

    val outputTree: Tree = inputTree match {
      case q"${inputExpression: Expression}" =>
        val liftedTree = q"$inputExpression"
        if (shouldDoTransform) {
          val transformer = new LocalIdentsTransformer(inputTree)
          transformer.transform(liftedTree)
        } else
          liftedTree
      case _ =>
        q"{ ${"unexpected input tree"} }"
    }
    println(s"Output tree: $outputTree")
    /*c.typecheck(c.untypecheck(*/outputTree/*))*/ // nothing commented helps without transforming (recreating) Idents
  }

  class LocalIdentsTransformer(initialTree: Tree) extends Transformer {
    // transform is mandatory in any case to relink (here: reset) Ident's owners when working with typechecked trees
    private val localDefSymbols: Set[Symbol] = initialTree.collect {
      case t if t != null && t.isDef && t.symbol.isTerm =>
        t.symbol
    }.toSet

    println("localDefSymbols", localDefSymbols)

    override def transform(tree: Tree): Tree = tree match {
      case tree@Ident(termName: TermName) if localDefSymbols.contains(tree.symbol) =>
        println("replacing local Ident", termName, tree.symbol)
        Ident(termName)
      case _ =>
        super.transform(tree)
    }
  }
}

def valMacro(doTransform: Boolean, doInitialUntypecheck: Boolean)(inputCode: Any): Any = macro ValMacro.valMacroImpl

val outerVal = 5
// 1) works with pre untypechecking, but we loose types info
valMacro(false, true) {
  val i = 1
  i + outerVal
}
// 2) works with Transformer
valMacro(true, false) {
  val i = 1
  i + outerVal
}
// 3) does not work
valMacro(false, false) {
  val i = 1
  i + outerVal
}
// 4) cases when we reuse old tree without changes: fails
valMacro(false, false) {
  var j = 1
  j
}
// 5) cases when we reuse old tree without changes: works
valMacro(true, false) {
  var j = 1
  j
}

输出:

// 1) works with pre untypechecking, but we loose types info
(Unlifted,OtherTree(null,1))
(Unlifted,SimpleVal(i,OtherTree(null,1)))
(Unlifted,OtherTree(null,i.+($line8.$read.INSTANCE.$iw.$iw.outerVal)))
(Unlifted,StatementsBlock(null,List(SimpleVal(i,OtherTree(null,1))),OtherTree(null,i.+($line8.$read.INSTANCE.$iw.$iw.outerVal))))
Output tree: {
  val i = 1.$plus(23);
  i.+($line8.$read.INSTANCE.$iw.$iw.outerVal)
}
res0: Any = 29

// 2) works with Transformer
Unlifted,OtherTree(Int(1),1))
(Unlifted,SimpleVal(i,OtherTree(Int(1),1)))
(Unlifted,OtherTree(Int,i.+($line8.$read.INSTANCE.$iw.$iw.outerVal)))
(Unlifted,StatementsBlock(Int,List(SimpleVal(i,OtherTree(Int(1),1))),OtherTree(Int,i.+($line8.$read.INSTANCE.$iw.$iw.outerVal))))
(localDefSymbols,Set(value i))
(replacing local Ident,i,value i)
Output tree: {
  val i = 1.$plus(23);
  i.+($line8.$read.INSTANCE.$iw.$iw.outerVal)
}
res1: Any = 29

// 3) does not work
(Unlifted,OtherTree(Int(1),1))
(Unlifted,SimpleVal(i,OtherTree(Int(1),1)))
(Unlifted,OtherTree(Int,i.+($line8.$read.INSTANCE.$iw.$iw.outerVal)))
(Unlifted,StatementsBlock(Int,List(SimpleVal(i,OtherTree(Int(1),1))),OtherTree(Int,i.+($line8.$read.INSTANCE.$iw.$iw.outerVal))))
Output tree: {
  val i = 1.$plus(23);
  i.+($line8.$read.INSTANCE.$iw.$iw.outerVal)
}

Error while emitting <console>
value i

// 4) case when we reuse old tree without changes: fails
(Unlifted,OtherTree(<notype>,var j: Int = 1))
(Unlifted,OtherTree(Int,j))
(Unlifted,StatementsBlock(Int,List(OtherTree(<notype>,var j: Int = 1)),OtherTree(Int,j)))
Output tree: {
  var j = 1;
  j
}

Error while emitting <console>
variable j

// 5) case when we reuse old tree without changes: works with Transformer
(Unlifted,OtherTree(<notype>,var j: Int = 1))
(Unlifted,OtherTree(Int,j))
(Unlifted,StatementsBlock(Int,List(OtherTree(<notype>,var j: Int = 1)),OtherTree(Int,j)))
(localDefSymbols,Set(variable j))
(replacing local Ident,j,variable j)
Output tree: {
  var j = 1;
  j
}

如果跳过 OtherTree 的 untypchecking(在提升它时)或不对结果树进行 untypchecking,那么我们将在 2) 示例宏调用中得到错误:

java.lang.AssertionError: assertion failed: 
  transformCaseApply: name = i tree = i / class scala.reflect.internal.Trees$Ident
     while compiling: <console>
        during phase: refchecks
     library version: version 2.12.12
    compiler version: version 2.12.12

实际上,这个示例展示了使用类型检查输入树的 2 种不同方法:

  1. 如何使用简单的 AST,它只覆盖了 Scala 语法树的一部分以及所需的类型集合(实际上可能不会使用)。如果不使用这样的 AST 并且结果树是使用源类型检查树的一部分(生成部分类型检查树)“即时”构建的,那么生成的树应该在发出之前取消类型检查(转换后)
  2. 如何使用 LocalIdentsTransformer 在部分类型检查的结果树中修复 Idents
相关问题