乐者为王

Do one thing, and do it well.

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

前文的语法仍然存在问题,因为它限制我们只能使用Java生成语法分析器。为使语法可复用和语言无关,我们需要完全避免嵌入动作。接下来就将展示如何用监听器做到这点。

使用语法分析树监听器实现应用

在构建语言应用时要避免应用和语法纠缠在一起,关键是让语法分析器生成语法分析树,然后遍历该树去触发特定的应用代码。我们可以使用我们最喜欢的技术遍历树,也可以使用ANTLR生成的树遍历机制中的一个。在本节中,我们将使用ANTLR内建的ParseTreeWalker构建一个基于监听器版本的属性文件应用。

让我们从属性文件语法的原始版本开始:

1
2
file : prop+ ;
prop : ID '=' STRING '\n' ;

下面是属性示例文件t.properties的内容:

1
2
user="parrt"
machine="maniac"

通过上述语法,ANTLR生成PropertyFileParser,该语法分析器会自动构建如下图所示的语法分析树:

有了语法分析树,我们就可以使用ParseTreeWalker去访问所有的节点,触发进入和退出方法。

现在来看一下ANTLR通过语法PropertyFile生成的监听器接口PropertyFileListener,当ANTLR的ParseTreeWalker发现和完成节点时,它会为每个规则子树分别触发进入和退出方法。因为在语法PropertyFile中只有两条语法规则,所以在接口中有4个方法:

1
2
3
4
5
6
public interface PropertyFileListener extends ParseTreeListener {
    void enterFile(PropertyFileParser.FileContext ctx);
    void exitFile(PropertyFileParser.FileContext ctx);
    void enterProp(PropertyFileParser.PropContext ctx);
    void exitProp(PropertyFileParser.PropContext ctx);
}

FileContext和PropContext对象是针对每条语法规则的语法分析树节点的实现,它们包含一些有用的方法。

为方便起见,ANTLR也会生成带有默认实现的类PropertyFileBaseListener,这些默认实现模仿在前文语法中@member区域我们手写的空白方法。

1
2
3
4
5
6
7
public class PropertyFileBaseVisitor<T> extends AbstractParseTreeVisitor<T>
                                        implements PropertyFileVisitor<T> {
    @Override
    public T visitFile(PropertyFileParser.FileContext ctx) { }
    @Override
    public T visitProp(PropertyFileParser.PropContext ctx) { }
}

默认实现让我们只需要覆盖和实现那些我们关心的方法。例如,以下是属性文件加载器的一个重新实现,像前面那样它只有一个方法,但使用监听器机制:

1
2
3
4
5
6
7
8
public static class PropertyFileLoader extends PropertyFileBaseListener {
    Map<String,String> props = new OrderedHashMap<String, String>();
    public void exitProp(PropertyFileParser.PropContext ctx) {
        String id = ctx.ID().getText();    // prop : ID '=' STRING '\n' ;
        String value = ctx.STRING().getText();
        props.put(id, value);
    }
}

主要的不同是这个版本扩展了基类监听器,而不是语法分析器,监听器方法在语法分析器完成后被触发。

这里面有很多的接口和类,让我们看一下在这些关键元素间的继承关系。

处于ANTLR运行库中的接口ParseTreeListener要求每个监听器对事件visitTerminal()、enterEveryRule()和exitEveryRule()作出反应,如果有语法错误的还要加上visitErrorNode()。ANTLR从语法文件PropertyFile生成接口PropertyFileListener,并且为类PropertyFileBaseListener的所有方法生成默认实现。我们仅需要构建PropertyFileLoader,它继承了PropertyFileBaseListener中的所有空方法。

方法exitProp()可以访问与规则prop相关的规则上下文对象PropContext,该上下文对象持有在规则prop中提到的每个元素(ID和STRING)的对应方法。因为这些元素是语法中的记号引用,所以方法返回语法分析树节点TerminalNode。我们既可以通过getText()直接访问记号的文本,也可以通过getSymbol()首先获取Token。

现在让我们创建测试文件TestPropertyFile.java遍历树,倾听来自PropertyFileLoader的声音:

1
2
3
4
5
6
// create a standard ANTLR parse tree walker
ParseTreeWalker walker = new ParseTreeWalker();
// create listener then feed to walker
PropertyFileLoader loader = new PropertyFileLoader();
walker.walk(loader, tree);    // walk parse tree
System.out.println(loader.props);    // print results

然后就是编译和生成代码,运行测试程序去处理输入文件:

1
2
3
antlr PropertyFile.g
compile *.java
run TestPropertyFile t.properties

这里是输出的内容:

1
{user="parrt", machine="maniac"}

测试程序成功地把文件中的属性赋值重组成内存中的映射数据结构。

Comments