你好 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 找不到任务或该任务依赖的类。”消息。)