GEP-2
摘要
Groovy 1.6 引入了执行本地和全局 AST(抽象语法树)转换的能力,允许用户在编译 Groovy 代码时读取和修改其 AST。在 Groovy 中读取 AST 中的信息相对容易。核心库提供了一个强类型访问者,称为 GroovyCodeVisitor。可以使用 ASTNode 子类型提供的 API 读取和修改节点。编写新的 AST 节点并不那么简单。从源代码生成的 AST 并不总是显而易见的,并且使用构造函数调用来生成节点树可能很冗长。本 GEP 提出了一种 ASTBuilder 对象,允许用户轻松创建 AST。
ASTBuilder 对象允许从以下方面创建 AST
-
包含 Groovy 源代码的字符串
-
包含 Groovy 源代码的闭包
-
包含 AST 创建 DSL 的闭包
这三种方法都共享相同的 API:实例化一个构建器对象并调用 build* 方法。
方法
来自字符串的 ASTNode
最简单的实现方法是提供一个 AST 构建器,其 API 接受一个字符串并返回 List<ASTNode>
def builder = new AstBuilder()
List<ASTNode> statements = builder.buildFromString(
CompilePhase.CONVERSION,
true,
""" println "Hello World" """
)
-
phase 参数告诉构建器从哪个阶段返回 AST。此参数是可选的,默认的 CompilePhase 是 CLASS_GENERATION。这为常见情况提供了更简洁的 API,并且选择 CLASS_GENERATION 是因为在后面的阶段中可以使用更多类型。
-
“statementsOnly” 布尔参数是一个可选参数,它告诉构建器丢弃生成的顶层 Script ClassNode。默认值为 true。
-
最后一个 String 参数是输入
-
构建器返回 List<ASTNode>
上面的例子产生了以下 AST
BlockStatement
-> ExpressionStatement
-> MethodCallExpression
-> VariableExpression
-> ConstantExpression
-> ArgumentListExpression
-> ConstantExpression
备选方案
-
此功能考虑过某种 AST 模板。请考虑以下示例
def astTemplate = builder.buildAst ( "println $txt" ).head()
def constant = builder.buildAst ( "To be, or not to be: that is the question" ).head()
def methodCallExpression = astTemplate.apply(txt: constant)
// method call expression not contains println "To be ... "
这种模板方法增加了可能不会使用的复杂性。它重载了 GString $ 运算符,因为它在这里仅与 ASTNode 类型的对象一起使用,但在 GString 中与任何 Object 类型一起正常使用。此外,模板方法可能会造成优先级混淆。假设 source = "$expr * y",然后 $expr 被绑定到 "x+a"。结果是 "x + a * y",这可能是无意的。目前,AST 构建器不包含此功能。
来自代码块的 ASTNode
一个有用的 API 将是使用代码块创建 AST。
AstBuilder builder = new AstBuilder()
def statementBlock = builder.buildFromCode (CompilePhase.CONVERSION, true) {
println "Hello World"
}
-
在 Groovy 源代码中表达 Groovy 源代码似乎是最自然的方法(而不是将 Groovy 源代码放入字符串中)。
-
一些 IDE 支持自然可用(突出显示等),但 IDE 警告对于变量作用域规则将产生误导。
-
来自“来自字符串的 ASTNode” 的相同问题和规则适用于 phase 和 statementsOnly 属性。
-
提供与来自字符串的构建器类似的 API,不同之处在于 code 属性接受任何在 Closure 上下文中有效的代码块。
-
从闭包转换为 AST 是通过全局编译转换执行的。这要求 AstBuilder 引用是强类型的,以便可以触发全局注释。
上面的例子产生了以下 AST
BlockStatement
-> ExpressionStatement
-> MethodCallExpression
-> VariableExpression
-> ConstantExpression
-> ArgumentListExpression
-> ConstantExpression
备选方案
如果使用 @ASTSource 注释,那么让用户在构建器之外重用该注释将非常容易。请考虑以下示例
@AstSource(CompilePhase.CONVERSION)
List<ASTNode> source = { println "compiled on: ${new Date()}" }
此选项似乎很有用;但是,不支持局部变量上的注释。这种方法不会被实现。
来自伪规范的 ASTNode
有条件地构建 AST,例如插入 if 语句或循环,在基于字符串或代码的构建器中不容易实现。请考虑此示例
def builder = new AstBuilder()
List<ASTNode> statements = builder.buildFromSpec {
methodCall {
variable "this"
constant "println"
argumentList {
if (locale == "US") constant "Hello"
if (locale == "FR") constant "Bonjour"
else constant "Ni hao"
}
}
}
此库类很有用,原因如下
-
在 AST 构建器中使用条件或循环可能很常见
-
在任何其他方法中都很难创建 Field 或 Method 引用
-
仅仅使用 @Newify 注释并不能充分改善语法
-
此结构消除了区分 Statement 和 Expressions 的需要,因为这些词已从方法名称中删除
-
这种方法不需要 phase 或 statementsOnly 属性
-
许多表达式接受类型 ClassNode,它包装了一个 Class。ClassNode 的语法是只传递一个 Class 实例,构建器会自动将其包装在 ClassNode 中。
问题
-
在 ASTNode 子类型上,构造函数参数列表可能很长,这种方法消除了 IDE 帮助的可能性。这是使用构建器需要付出的代价,计划在 1.7 中推出的构建器元数据功能可能会缓解这种情况。
-
从伪规范创建 AST 的类应该实现,使其不会创建当前 AST 类型的镜像类层次结构。这将迫使对 AST 类型的更改在两个地方执行:一次在 ASTNode 子类中,一次在该构建器中。如果不可能,那么至少 AST 层次结构不应频繁更改。
-
几个 ASTNode 类型具有完全相同的类型构造函数签名:(Expression, Expression, Expression) 最常见。这意味着 DSL 中的参数是顺序相关的,以错误顺序指定参数不会创建异常,但在运行时会导致截然不同的结果。这在邮件列表中已完全记录。
-
指定 Parameter 对象的语法在邮件列表中已有记录。
-
一些 ASTNode 类型与语言关键字存在命名冲突。例如,ClassExpression 类型不能缩写为 'class',IfStatement 不能缩写为 'if'。这在邮件列表中已完全记录。
-
参数具有默认值,可以是可变参数。需要提出合适的语法。
-
有时需要在 DSL 中切换构造函数参数的顺序。例如,考虑 SwitchStatement(Expression expression, List<CaseStatement> caseStatements, Expression defaultStatement)。DSL 的当前语法对参数施加了一种 VarArgs 严格性:列表只是由重复元素隐含的。因此,让 SwitchStatement 的中间参数成为一个列表是有问题的,因为转换构造函数的自然方法是让它变成 (Expression expression, CaseStatement… caseStatements, Expression default),这是不可能的。这在邮件列表中已完全记录。
备选方案
Template Haskell 和 Boo 为 AST 构建语句提供了一种特殊语法。可以使用 Quasi-quote(或牛津引号)来触发 AST 构建操作
ConstantExpression exp = [| "Hello World" |]
这些语言还提供了一个拼接运算符 $(…) 来将 AST 转换回代码。这不是 AstBuilder 工作的一部分。
参考资料和有用链接
Cython 元编程提案 由 Martin C Martin 撰写 - 包含了一些用例的优秀说明
JIRA 问题
此功能依赖于允许在局部变量上使用注释:GROOVY-3481
更新历史
- 7 (2009-06-17)
-
从 Codehaus wiki 中提取的版本
- 8 (2018-10-11)
-
添加了关于宏的评论