你好,杯子

2007年5月13日

当我探索用于外部领域特定语言的解析器生成器工具时,我已经说过HelloAntlrHelloSablecc。如果你花很多时间研究解析器生成器,你不可能不关注那些老牌的工具,比如lex 和 yacc(或者它们的 GNU 版本 flex 和 bison)。我想探索 lex 和 yacc 的工作方式,但我的 C 语言已经生疏了。正如 Erich Gamma 所说,我已经懒得自己清理垃圾了。幸运的是,有一个 Java 版本的 yacc 系统,这正是我需要的。

Java 实现,就像经典的 lex 和 yacc 一样,是两个独立的工具:JFlexCUP。虽然它们是分开开发的,但它们确实提供了相互协作的钩子。

与我之前类似的帖子一样,这是一个过于简单的示例,只是为了让工具正常工作。我使用一个输入文件,其中包含以下内容:

item camera
item laser

并将它们转换为配置中的项目对象,使用以下模型:

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"));
    }

第一个问题是让构建运行起来。与我之前的示例一样,我想将语法输入文件生成到 gen 目录中的词法分析器和解析器。与我之前的示例不同,我没有直接在 ant 中执行此操作,而是使用 ant 调用一个 ruby 脚本。

--- build.xml
 <target name = "gen" >
    <exec executable="ruby" failonerror="true">
      <arg line = "gen.rb"/>
    </exec>
  </target>

--- gen.rb
require 'fileutils'
include FileUtils

system "java -cp lib/JFlex.jar JFlex.Main -d gen/parser src/parser/catalog.l"

system "java -jar lib/java-cup-11a.jar src/parser/catalog.y"
%w[parser.java sym.java].each {|f| mv f, 'gen/parser'} 

是的,我知道这有点绕,但对于很多源文件,我使用 FlexibleAntlrGeneration 中的方法来进行生成,我不愿意在 ant 中也进行排序。

(当我最近参加 CITCON 时,我惊讶地发现人们对 ant 的满意程度远超我的预期。我这个脾气暴躁的人认为这是一种斯德哥尔摩综合征。即使在我心情不那么暴躁的时候,我也一直在关注像 RavenBuildR 这样的工具,现在它们已经有了文档。我迫不及待地想抛弃 ant。)

你会注意到 CUP 将其输出文件放在当前目录中,我无法找到覆盖该行为的方法。因此,我生成了它们,并使用单独的命令将它们移动。

生成代码后,我使用 ant 对其进行编译和测试。

<target name = "compile" depends = "gen">
    <mkdir dir="${dir.build}"/>
    <javac destdir="${dir.build}" classpathref="path.compile">
      <src path = "${dir.src}"/>
      <src path = "${dir.gen}"/>
      <src path = "${dir.test}"/>
    </javac>
  </target>

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

Lex 和 yacc 将词法分析器和解析器分离到不同的文件中。每个文件都是独立生成的,并在编译期间组合在一起。我将从词法分析器文件 (catalog.l) 开始。开头声明了输出文件的包和导入。

package parser;
import java_cup.runtime.*;

JFlex 使用 %% 标记将文件分成几部分。第二部分包含各种声明。第一部分命名输出类,并告诉它与 CUP 交互。

%%
%class Lexer
%cup

下一部分是折叠到词法分析器中的代码。在这里,我定义了一个函数来创建 Symbol 对象 - 再次与 CUP 钩子连接。

%{
  private Symbol symbol(int type) {
    return new Symbol(type, yytext());
  }
%}

Symbol 类在 CUP 中定义,是其运行时 jar 的一部分。它有各种构造函数,接受有关符号及其位置的各种信息。

接下来是一些宏,用于定义单词和空格。

Word = [:jletter:]*
WS = [ \t\r\n]

最后一部分是实际的词法分析器规则。我定义了一个规则来返回 item 关键字,另一个规则将简单单词返回给解析器。

%%
"item"      {return symbol(sym.K_ITEM);}
{Word}      {return symbol(sym.WORD);}
{WS}        {/* ignore */}

因此,词法分析器将向解析器发送 K_ITEMWORD 标记流。我在 catalog.y 中定义了解析器。同样,它从包和导入声明开始。

package parser;
import java_cup.runtime.*;
import model.*;

我将数据解析到一个配置对象中,因此我需要声明一个地方来放置该结果。同样,这段代码直接复制到解析器对象中。

parser code {: Configuration result = new Configuration(); :}

在 CUP 中,我需要定义所有将在生成式中使用的规则元素。

terminal K_ITEM;
terminal String WORD;
non terminal  catalog, item;

终结符是我从词法分析器获得的标记,非终结符是我自己构建的规则。如果我想从标记中获取有效负载,我需要声明其类型,因此 WORD 是一个字符串。

目录是项目的列表。与 antlr 或 sablecc 不同,这里没有 EBNF,所以我不能说 item*,而是需要一个递归规则。

catalog ::= item | item catalog;

item 规则本身包含将项目放入配置中的嵌入式操作。

item ::= K_ITEM WORD:w {: parser.result.addItem(new Item(w)); :}
          ;

这里需要注意的一个小问题是,操作被放入一个与解析器对象不同的类中,因此要访问之前定义的 result 字段,我必须使用操作对象的 parser 字段。我还应该提到,一旦我在这方面做得更多,我就会开始使用 EmbedmentHelper 来保持操作代码的简单性。

使用过 yacc 的人可能会注意到,我可以标记规则的元素,以便在操作中引用它们,而不是 yacc 中使用的 $1$2 约定。类似地,如果规则返回一个值,CUP 使用 RESULT 而不是 $$

我对 lex 和 yacc 的记忆很模糊,但这些工具似乎很好地模仿了使用它们的方式。到目前为止,我最大的抱怨是错误处理,它给我带来的麻烦比 antlr 多得多。到目前为止,我的感觉是,如果你不熟悉解析器生成器,那么 antlr 是更好的选择(尤其是由于它的 )。但是,如果你熟悉 lex 和 yacc,那么这两个工具足够相似,可以让你利用这些知识。