你好 Sablecc
2007 年 2 月 11 日
我最近玩了一点 SableCC。让一个“Hello World”风格的解析器运行起来需要一些努力,所以我认为我应该在这里记录一下我让它工作的方法。我并不是说这是最好的方法,但如果你想玩玩它,它可能会有用。
SableCC 是一个用于 Java 环境的编译器编译器工具。它处理 LALR(1) 语法(对于那些记得语法类别的人来说)。换句话说,它是一个自下而上的解析器(不像 JavaCC 和 Antlr,它们是自上而下的)。
与大多数编译器编译器工具一样,您需要在语法文件中定义语言的语法,然后运行编译器编译器为该语言生成解析器。由于这是一个 hello-world 示例,我的语言只是一个最小的语言,只是为了让编译器编译器运行起来。该语言只是一个像这样的项目列表
item camera item laser
其中“item”是一个关键字,第二个词是一个名称。我希望解析器将此转换为 Configuration 类的实例,该实例包含一个项目列表。
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 itemsAddedToItemList() { Reader input = null; try { input = new FileReader("rules.txt"); } catch (FileNotFoundException e) { throw new RuntimeException(e); } Configuration config = CatalogParser.parse(input); assertNotNull(config.getItem("camera")); assertNotNull(config.getItem("laser")); }
当然,在这种情况下使用编译器编译器是完全过度的,但 hello-world 示例的目的是让环境在简单的情况下工作,这样你就可以在基础工作的情况下继续进行有趣的情况。每当我玩一种新技术时,我总是喜欢先让这样的东西运行起来,然后再深入研究有趣的部分。
使用编译器编译器会使构建过程更加复杂。您必须首先在语法文件上运行编译器编译器以生成解析器,然后编译您的自定义代码和生成的解析器以创建整个程序,然后运行(并测试它)。所以在这个时候,我不能只在我的 IntelliJ 中完成所有事情,实际上必须创建一个 ant 文件。我已经有一段时间没有创建 ant 文件了,所以花了我一段时间才重新记住如何使用 ant 语言。为了运行 SableCC,我使用了 Java 任务
<property name = "gendir" value = "gen"/> <target name = "gen" > <mkdir dir="${gendir}"/> <java jar = "lib/sablecc.jar" fork = "true" failonerror="true"> <arg value = "-d"/> <arg value = "${gendir}"/> <arg value = "catalog.sable"/> </java> </target>
我将代码生成到 gen
目录中,以将其与我在 src
和 test
目录中自己编写的代码分开。然后我用 javac 任务将它们全部编译在一起。
<property name = "builddir" value = "classes/production/sable"/> <path id="classpath"> <fileset dir = "lib"> <include name = "*.jar"/> </fileset> <pathelement path = "${builddir}"/> </path> <target name = "compile" depends = "gen, copyDats"> <mkdir dir="${builddir}"/> <javac destdir="${builddir}" classpathref="classpath"> <src path = "src"/> <src path = "${gendir}"/> <src path = "test"/> </javac> </target>
除了编译之外,我还必须将两个解析器数据文件移动到构建目录中。该数据包含用于解析器和词法分析器的表。构建目录以我的方式嵌套,因此它可以很好地与 IntelliJ 协同工作。(我真的应该将测试也分离到一个单独的输出目录中,但我当时感觉很懒。)
<target name = "copyDats"> <mkdir dir="${builddir}"/> <copy todir = "${builddir}"> <fileset dir = "${gendir}" includes = "**/lexer.dat"/> <fileset dir = "${gendir}" includes = "**/parser.dat"/> </copy> </target>
然后我有一个测试任务来运行测试。
<target name = "test" depends="compile"> <junit haltonfailure = "on"> <formatter type="brief"/> <classpath refid = "classpath"/> <batchtest todir="${builddir}" > <fileset dir = "test" includes = "**/*Tester.java"/> </batchtest> </junit> </target>
为了让解析器运行起来,我需要使用 SableCC 的语法语法定义我简单语言的语法。
Package catalogParser; Tokens itemdef = 'item'; string = ['a' .. 'z'] +; blank = (' ' | 13 | 10)+; Ignored Tokens blank; Productions configuration = item * ; item = itemdef string ;
与大多数编译器编译器一样,SableCC 将工作分为词法分析器和解析器。词法分析器读取字符并将它们分成由语法文件 Tokens
部分定义的标记。在这种情况下,它非常简单:字符串是小写字母,关键字 item
是它自己的标记。我还定义了什么为空白,并告诉词法分析器在 Ignores
子句中将其丢弃。
然后词法分析器将向解析器提供 itemdef
和 string
标记流。解析器使用两个产生式来处理这个问题。它将配置描述为多个项目,并将每个项目描述为一个 itemdef 和一个字符串(用于其名称)。
这定义了我输入的语法,但没有说明如何从输入获取我的配置和项目对象。为了做到这一点,我需要编写一些代码来映射我解析的内容和我想创建的对象。在大多数编译器编译器中,我通过将操作嵌入到语法中来做到这一点。然而,SableCC 以另一种方式工作。它会自动创建一个解析树,然后给我一个访问器来遍历这棵解析树。然后我可以对访问器进行子类化以执行有趣的事情。在这种情况下,当我遍历解析树时,我会获取解析树上的每个项目节点,并将其转换为模型中的真实项目。
public class CatalogParser extends DepthFirstAdapter { private Configuration result; public void outAItem(AItem itemNode) { System.out.println("found item"); result.addItem(new Item(itemNode.getString().toString().trim())); }
解析树使用命名约定将语法绑定到在树中创建的对象。因此,语法创建名为 AItem
的节点以匹配语法中的项目产生式。当访问器离开项目节点时,会调用方法 outAItem
,并允许我访问项目上的任何内容,在这种情况下是底层字符串标记。然后,我可以使用该字符串作为名称在我的模型中创建项目。
最后一段代码是在文件上运行解析器,我通过将目录解析器设为命令对象来实现。
public static Configuration parse(Reader input) { Configuration result = new Configuration(); new CatalogParser(input, result).run(); return result; } public CatalogParser(Reader input, Configuration result) { this.input = input; this.result = result; } public void run() { try { createParser(input).parse().apply(this); } catch (Exception e) { throw new RuntimeException(e); } } private Parser createParser(Reader input) { return new Parser(new Lexer(new PushbackReader(input))); }
所以这就是让它运行起来的基础。到目前为止,我还没有开始深入研究,所以我对 SableCC 的想法还有些初步,但这就是这个博客的全部意义,就是写一些半成品的想法。
SableCC 使用起来有点笨拙。除了作者的论文之外,几乎没有文档。幸运的是,这篇论文比我遇到的许多其他论文更容易理解,所以我能够弄清楚如何让事情运行起来。在我的工作中,我在语法中犯了一个错误,发现很难获得诊断信息。错误消息不太有信息量,我求助于调试器和生成的解析器代码中的打印语句。幸运的是,问题出在标记化上,所以我通过查看词法分析器的输出来意识到我的错误。LALR 解析器以其难以理解而闻名,所以我很高兴我不必深入研究。Antlr 在这方面得分更高。递归下降解析器更容易理解,而且有一本 Antlr 书 正在编写中,这将在我探索它时对我有所帮助。
到目前为止,我还没有被移除解析器操作并自动生成解析树的方法说服。由于它是一个解析树,您必须遍历它才能做任何有用的事情。Nat Pryce 让我了解了最新 SableCC 中的 树转换规则,这些规则看起来更有用,因为它定义了一个抽象语法树而不是一个解析树。您仍然必须遍历它才能在域模型中创建对象,但它更容易遍历。(最新版本的 Antlr 具有类似的功能。)树遍历的一个优点是,如果我对树遍历进行更改,我不需要重新生成 - 这让我留在 IntelliJ 中。但是,Antlr 有 AntlrWorks,它可以插入 IntelliJ,看起来非常棒。