我正在尝试编写一个Scala编译器插件,它允许极其通用的代码生成:类似于C预处理器的通用性,但有点类型安全(我不确定这是否是一个可怕的想法,但它是一个有趣的运动)。我理想的用例看起来像这样:
// User code. This represents some function that might take some args
// and outputs an abstract syntax tree.
def createFooTree(...): scala.reflect.runtime.universe.Tree = ...
// Later user code (maybe separate compilation?). Here the user generates
// code programmatically using the function call to |createFooTree| and inserts
// the code using insertTree.
insertTree(createFooTree(...))
重要的插件代码可能如下所示(基于this):
class InsertTreeComponent(val global: Global)
extends PluginComponent
with TypingTransformers {
import global._
import definitions._
override val phaseName = "insertTree"
override val runsRightAfter = Some("parser")
override val runsAfter = runsRightAfter.toList
override val runsBefore = List[String]("typer")
def newPhase(prev: Phase): StdPhase = new StdPhase(prev) {
def apply(unit: CompilationUnit) {
val onTransformer = new TypingTransformer(unit) {
override def transform(tree: Tree): Tree = tree match {
case orig @ Apply(
function,
// |treeClosure| is the closure we passed, which should
// evaluate to a Tree (albeit a runtime Tree).
// The function.toString bit matches anything that looks like a
// function call with a function called |insertTree|.
treeClosure) if (function.toString == "insertTree") => {
// This function evaluates and returns the Tree, inserting it
// into the call site as automatically-generated code.
// Unfortunately, the following line isn't valid.
eval(treeClosure): Tree
}
...
知道怎么做吗?请不要说“只使用宏”;至少在2.10中,它们不够通用。
顺便说一句,我看到我概述的方法有两个问题:1)编译器插件采用AST,而不是闭包。它需要一些创建闭包的方法,可能会在用户代码上添加构建依赖项。 2)用户无法访问scala.reflect.internal.Trees.Tree,只能访问scala.reflect.runtime.universe.Tree,因此插件需要在两者之间进行转换。答案 0 :(得分:9)
您面临的实施困难部分是2.10中的宏不够通用的原因。它们看起来非常具有挑战性,甚至是根本性的,但我乐观地认为它们最终会被击败。以下是一些棘手的设计问题:
1)您如何知道您正在呼叫的功能是正确的insertTree
?如果用户编写了自己的名为insertTree
的函数,那么如何区分魔术调用与特殊函数和正常调用用户定义函数?确保您需要检查对该函数的引用。但这并不容易(见下文)。
2)你究竟如何评价createFooTree(...)
电话?和以前一样,你需要对createFooTree
部分进行类型检查以找出它代表什么,这并不容易。
3)然后还有一个问题。如果您正在编译的其中一个文件中定义了createFooTree
,该怎么办?然后你会以某种方式将它和它的依赖项与程序的其余部分分开,将它放入不同的编译运行,编译然后调用它。然后,如果函数或其中一个依赖项的编译导致宏扩展,这应该改变编译器的某些全局状态。我们如何将它传播到程序的其余部分?
4)我一直在谈论类型检查。那是问题吗?显然,是的。如果您的宏可以扩展到任何地方,那么类型检查变得非常棘手。例如,你如何进行类型检查:
class C {
insertTree(createFoo(bar)) // creates `def foo = 2`, requires `bar` to be defined to operate
insertTree(createBar(foo)) // creates `def bar = 4`, requires `foo` to be defined to operate
}
然而,好消息是你不必使用scala.reflect.runtime.universe.Tree
。您可以createFooTree
依赖类型:def createFooTree[U <: scala.reflect.api.Universe with Singleton](u: Universe): u.Tree
。这个,或者我们在Scala 2.10中使用scala.reflect.macros.Context
的方法。不是很漂亮,但解决了宇宙不匹配的问题。
作为一个底线,我目前的感觉是静态类型语言中的宏(特别是在面向对象的语言中,因为OO为代码片段相互依赖带来了一堆惊人的方法)真的很棘手。尚未发现用于修改正在编译的程序中的任意片段的类型宏的鲁棒模型。
如果您希望我们可以通过电子邮件进行更详细的讨论。我们还可以合作,使适当的宏的想法成为现实。或者,如果您可以分享您的用例,我可以尝试帮助您找到适合您特定情况的解决方法。