乐者为王

Do one thing, and do it well.

ANTLR 4权威参考读书笔记(20)

单独的语法不是很有用,因为相关的语法分析器只能告诉我们某个输入句子是否遵守语言规格。为构建语言应用,我们需要语法分析器在看到特定的输入句子、短语或者记号时触发特定的动作。这些短语-动作对的集合表示语言应用或者至少是在语法和一个更大的周边应用之间的接口。

我们可以使用语法分析树监听器和访问者构建语言应用。监听器是一个响应规则进入和退出事件的对象,这些短语识别事件在语法分析树遍历器发现和完成节点时触发。为了支持应用必须控制树是如何被遍历的这种情况,ANTLR生成的语法分析树也支持著名的树访问者模式。

监听器和访问者之间最大的不同是,监听器方法不对显式地调用方法去遍历它们的子树负责,而访问者必须显式地触发访问子节点以保持树的遍历继续。通过这些显式的访问子树的调用,访问者控制遍历的顺序以及多少树被访问。为方便起见,我们使用术语“事件方法”来指代监听器回调或者访问者方法。

为了确切地知道ANTLR为我们构建了什么样的树遍历设施以及为什么,让我们首先看看监听器机制的起源以及如何使用监听器和访问者把特定应用的代码和语法分离。

从嵌入动作演化到监听器

监听器和访问者机制将语法从应用代码中解耦,提供了一些令人信服的好处。这种解耦很好地封装了应用程序,而不是把它切割成碎片分散到语法中的各个地方。没有嵌入动作,我们可以在不同的应用中复用相同的语法,甚至不需要重新编译生成的语法分析器。如果没有嵌入动作,ANTLR还可以用相同的语法生成不同编程语言的语法分析器。集成语法缺陷修复或更新也更容易,因为我们不必担心嵌入动作导致的合并冲突。

接下来,我们将探讨从带有嵌入动作的语法到完全解耦的语法和应用的演化。以下含有用«...»描述的嵌入动作的属性文件语法读取属性文件,每行一个属性赋值。像«start file»这样的动作只是Java代码适当的替代。

1
2
3
4
5
grammar PropertyFile;
file : {«start file»} prop+ {«finish file»} ;
prop : ID '=' STRING '\n' {«process property»} ;
ID   : [a-z]+ ;
STRING : '"' .*? '"' ;

这样的紧密耦合把语法束缚到一个特定的应用上。一个更好的方法是创建由ANTLR生成的语法分析器PropertyFileParser的子类,然后把嵌入动作转换成方法。重构仅留下不重要的方法调用在语法中触发新创建方法的动作,然后,通过子类化语法分析器,我们可以实现任意数量的不同应用而无需修改语法。这样的重构看起来像:

1
2
3
4
5
6
7
8
9
10
11
grammar PropertyFile;
@members {
    void startFile() { }    // blank implementations
    void finishFile() { }
    void defineProperty(Token name, Token value) { }
}

file : {startFile();} prop+ {finishFile();} ;
prop : ID '=' STRING '\n' {defineProperty($ID, $STRING)} ;
ID   : [a-z]+ ;
STRING : '"' .*? '"' ;

解耦可以让语法被不同的应用复用,但语法因为方法调用的关系仍然被绑定在Java上。

为演示已重构语法的复用性,让我们构建两个不同的应用。首先从遇到属性后只是打印它们的那个开始。这个过程只是去扩展由ANTLR生成的语法分析器类和覆盖一个或多个由语法触发的方法。

1
2
3
4
5
class PropertyFilePrinter extends PropertyFileParser {
    void defineProperty(Token name, Token value) {
        System.out.println(name.getText() + "=" + value.getText());
    }
}

由于ANTLR生成的PropertyFileParser类中的默认实现,我们不需要覆盖startFile()或finishFile()。

为了启动这个应用,我们需要创建一个特定的PropertyFilePrinter语法分析器子类的实例,而不是常规的PropertyFileParser的。

1
2
3
4
PropertyFileLexer lexer = new PropertyFileLexer(input);
CommonTokenStream tokens = new CommonTokenStream(lexer);
PropertyFilePrinter parser = new PropertyFilePrinter(tokens);
parser.file();    // launch our special version of the parser

作为第二个应用,我们把属性加载到一个映射中而不是打印它们。我们需要做的就是创建一个新的子类并把不同的功能放到defineProperty()中。

1
2
3
4
5
6
class PropertyFileLoader extends PropertyFileParser {
    Map<String,String> props = new OrderedHashMap<String, String>();
    void defineProperty(Token name, Token value) {
        props.put(name.getText(), value.getText());
    }
}

在语法分析器执行完毕后,字段props将会包含名-值对。

Comments