你好 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 目录中,以将其与我在 srctest 目录中自己编写的代码分开。然后我用 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 子句中将其丢弃。

然后词法分析器将向解析器提供 itemdefstring 标记流。解析器使用两个产生式来处理这个问题。它将配置描述为多个项目,并将每个项目描述为一个 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,看起来非常棒。