你好 Antlr

2007 年 3 月 7 日

在说HelloSablecc之后,我还想尝试一下Antlr,它是 Java 空间中的另一个编译器编译器。与那篇文章一样,这只是关于用一个非常简单的“hello world”风格语法来启动 Antlr。

与 SableCC 一样,Antlr 也是一个编译器编译器工具。它已经存在了一段时间,我遇到过一些使用它的项目。与 SableCC(以及久负盛名的 lex/yacc 组合)不同,它使用 LL 语法生成递归下降解析器。编译器高手喜欢争论 LL 或 LALR 哪个更好,我在这里不会参与这场辩论。

我的简单案例是解析一个像这样的项目列表文件

item camera
item laser

每一行都有一个“item”关键字,后面跟着一个表示项目名称的单词。我将把每个项目对象加载到一个配置对象中,该对象将它们全部保存在一起。

public class Configuration {
  private Map<String, Item> items = new HashMap<String, Item>();
  public Item getItem(String key) {
    return items.get(key);
  }
  public void addItem(Item arg) {
    items.put(arg.getName(), arg);
  }
public class Item {
  private String name;
  public Item(String name) {
     this.name = name;
   }

这是一个使用上面显示的文件进行测试的示例。

 @Test public void readTwoItems() {
    Reader input = null;
    try {
      input = new FileReader("catalog.txt");
    } catch (FileNotFoundException e) {
      throw new RuntimeException(e);
    }
    Configuration config = ParserCommand.parse(input);
    assertNotNull(config.getItem("camera"));
    assertNotNull(config.getItem("laser"));
    assertEquals(2, config.getItems().size());
  }

和以前一样——在这个问题上使用编译器编译器很愚蠢,但打印控制台上的“hello world”也是如此。出于与我总是用新环境编写“hello world”相同的原因,我喜欢编写一些非常简单的东西,只是为了确保在开始使用它做任何实际的事情之前,我可以让它正常工作。

使用像这样的编译器编译器的一个麻烦是它使构建过程更加复杂。我必须在语法文件上运行 antlr 来创建解析器的 java 类,然后将它们包含在编译中。所以现在是时候再次与 ant 作斗争了——这是 ant 目标

  <property name = "dir.parser" value = "${dir.gen}/parser"/>
  <path id = "path.antlr">
    <fileset dir = "${dir.lib}">
      <include name = "antlr*.jar"/>
      <include name = "stringtemplate*.jar"/>
    </fileset>
  </path>
  <target name = "gen" >
    <mkdir dir="${dir.parser}"/>
    <java classname="org.antlr.Tool" classpathref="path.antlr" fork = "true" failonerror="true">
      <arg value="-o"/>
      <arg value="${dir.parser}"/>
      <arg value="Catalog.g"/>
     </java>
  </target>

这会将代码生成到 gen 目录中。这样,生成的代码就与我自己编写的源代码分离了。另一个目标是进行编译

 <property name = "dir.build" value = "classes/production/antlrLair"/> 
 <target name = "compile" depends = "gen">
    <mkdir dir="${dir.build}"/>
    <javac destdir="${dir.build}" classpathref="classpath">
      <src path = "src"/>
      <src path = "${dir.gen}"/>
      <src path = "test"/>
    </javac>
  </target>

然后,我可以使用最终目标运行测试。

<target name = "test" depends="compile">
    <junit haltonfailure = "on">
      <formatter type="brief"/>
      <classpath refid = "classpath"/>
      <batchtest todir="${dir.build}" >
        <fileset dir = "test" includes = "**/*Test.java"/>
      </batchtest>
     </junit>
   </target>

Antlr 使用语法文件 Catalog.g。语法文件定义了语法中的产生式,以及解析器在遇到产生式时采取的操作。语法文件还定义了词法分析器(如果需要,可以将它们分开)。从这个意义上说,Antlr 比 SableCC 更传统(也更灵活)。SableCC 不允许操作,而是生成一个解析树或 AST,并用 java 遍历它。Antlr 允许任意操作,或者它支持以与 SableCC 相同的方式构建树。(Antlr 还使用语法文件来遍历树。)由于我正在构建一个简单的项目域模型和配置,因此我将放弃树构建,并在我的操作中完成所有工作。

我将分块地介绍这个文件,并在介绍过程中进行描述。我从语法标题开始

grammar Catalog;

Antlr 支持在多个点将代码注入生成的解析器(而不是生成 SableCC 所做的超类)。我把包声明和导入放在标题中。

@header{
package parser;
import model.*;
}
@lexer::header {
package parser;
}

下一个代码注入是将代码放在生成的类的主体中。本质上,这会向类添加成员,因此命令的名称。

@members {
  public Configuration result = new Configuration();
}

现在我可以进入语法的产生式了。我将自上而下地进行,因为它是一个自上而下的解析器。我首先说目录由多个 item 子句组成,后面跟着文件结尾。

catalog :  item* EOF;

接下来,我将项目子句定义为文字字符串“item”,后面跟着一个字符串。

item 	: 'item'  name=STRING 
   {result.addItem(new Item ($name.text));};

在这里,我还添加了操作,即在模型中创建一个新项目,并将名称设置为字符串的值。花括号内的代码是 java 代码,在识别该术语后添加到解析器中。我可以命名产生式中的元素,然后在操作中引用它们。在这里,我将字符串命名为“name”,这在上下文中是有意义的,即使它会导致一个笨拙的写法。

最后的产生式定义了字符串和空白的词法分析器元素。

STRING 	: ('a'..'z' | 'A'..'Z')+ ;
WS : (' ' |'\t' | '\r' | '\n')+ {skip();} ;

空白的操作是跳过(忽略)它。

有一些事情使 Antlr 比 SableCC 更易于使用。Antlr 有一个名为 AntlrWorks 的不错的 IDE,可以插入 IntelliJ。该工具将为您提供语法元素的语法高亮和完成,绘制语法的语法图,并允许您输入要解析的测试片段——显示生成的解析树。这是一个非常有用的工具,可以查看解析器正在做什么。但是,操作中的代码没有高亮/完成,这是一个可以理解的痛苦。

Antlr 的另一个优点是有一本关于它的 不错的书 正在编写中。这本书详细介绍了该工具的工作原理,以及关于语言和编译器原理的有用背景知识。它假设您正在处理一个完整的语言,并且您将生成代码——对于 DSL 工作来说并不一定如此。但是,它提供的细节看起来将在我深入研究时非常宝贵。

如果您想填充模型,Antlr 的操作似乎是一个更容易的选择——我不确定中间解析树或 AST 在这里有多有用。同样,进一步的调查将让我对它有更好的感觉。语言越复杂,中间树表示就越有用。我喜欢 Antlr 在允许您执行操作或使用转换进行树构建方面的灵活性。

不可避免地,即使在这个简单的例子中,我也遇到了问题。我最大的障碍是我最初将目录项定义为 catalog : item*;,也就是说没有 EOF。然后我感到困惑,因为解析器在遇到多余的输入(如 xitem foo)时没有指示错误。Antlr 和 AntlrWorks 之间的差异并没有帮助(后者确实显示了错误,并且 AntlrWorks 的旧版本也会以不同的方式处理空白。)

(另一个大麻烦是让 ant 和 JUnit 正常工作。我不想考虑多年来我花费在诊断类路径问题上的时间,尤其是对于 臭名昭著的“Ant 找不到任务或该任务依赖的类。”消息。)