GEP-2


元数据
编号

GEP-2

标题

AST 构建器支持

版本

8

类型

功能

目标

Groovy 1.7

状态

最终

评论

已包含在 Groovy 中,但某种程度上*已被宏取代*

负责人

Hamlet D'Arcy

创建

2009-04-01

最后修改 

2018-10-12

摘要

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)

添加了关于宏的评论