写在之前
之前我们已经通过两个例子尝试着解决过歧义性的问题:
-
我们通过语法分析器
优先匹配靠前的规则这一准则,将乘法设置在加法规则之前,来解决这个问题。

但问题是,这种优先性的歧义是在语法分析树遍历时产生的,如果我们在词法分析或者语法分析过程就遇到歧义的规则呢? -
在Keywords.g4中,我们设置了IF、WHILE等多个
关键字。但在词法分析过程中,它们会与ID:[a-zA-z]这种常见的标识符词法规则产生歧义。后来我们通过在ID规则的内嵌动作中多添加了一次判定,判定ID是否为关键字,是的话将ID类型换成对应的关键字类型,再进行文法规则匹配。ID : [a-zA-Z]+ { if(keywords.containsKey(getText())) { setType(keywords.get(getText())); } } ;
词法分析时的歧义改进版
在之前的词法分析歧义中,我们通过在词法分析器中加入map< 关键字,Token类型 >的方式,在ID的词法分析中检测有无
已存的关键字,并将该关键字的类型从ID转换成关键字Token类型。
T__0=1
T__1=2
ID=3
CHAR=4
INT=5
WS=6
BEGIN=7
END=8
IF=9
THEN=10
WHILE=11
'='=1
';'=2
我们可以看到,由于BEGIN、END、IF、THEN、WHILE是通过tokens{}的方式添加的,因此在显式的词法规则之后。而=和;是在文法规则中隐式添加的,其类型排在最前面。
接下来我们将介绍如何去掉这些内嵌动作和预成员,直接通过隐式的先后顺序解决词法歧义性。
预想效果
来看一下一个让人头疼的输入吧:
if if then call call;
我们想让第一个if匹配成关键字,第二个if匹配成标识符,第一个call匹配成关键字,第二个call匹配成标识符。
语法文件
让我们来看看做了什么。
if then call 直接在文法规则stat中出现了,这意味着它们隐式地生成了token。让我们看看tokens表:
T__0=1
T__1=2
T__2=3
T__3=4
ID=5
WS=6
'if'=1
'then'=2
'call'=3
';'=4
果然它们的类型在显式词法规则之前了。接下去我们观察id,它包含了3个隐式token和1个词法规则。
id : 'if' | 'call' | 'then' | ID ;
这意味着“if”、"then"、“call”从ID规则中剥离了出来,lexer会把它们单独的作为token类型来看,而且还在ID标识符token类型之前。
由于ANTLR4的分析顺序为 字符输入流->词法分析->tokens流->语法分析,这几个关键字token类型会提前被匹配。
grammar IDKeyword;
prog: stat+ ;
stat: 'if' expr 'then' stat
| 'call' id ';'
| ';'
;
expr: id ;
id : 'if' | 'call' | 'then' | ID ;
ID : [a-z]+ ;
WS : [ \r\n]+ -> skip ;
运行结果

语法分析时的歧义
接下来我们通过一个C++中简单的例子来演示如何解决语法分析时的歧义。
语法文件
我们可以发现,decl规则中的第二条分支和expr规则中的第三条分支会造成歧义:
比如输入T(i),它会被匹配成T类型的变量声明,也会被匹配成T函数调用(i作为实参)。
我们可以通过ANTL4的优先匹配靠前规则的机制,使得这种歧义总是调用decl规则。
但有时我们也可以通过自定义的方法来精准匹配。
grammar CppStat;
stat: decl ';' {System.out.println("decl "+$decl.text);}
| expr ';' {System.out.println("expr "+$expr.text);}
;
decl: ID ID // E.g., "Point p"
| ID '(' ID ')' // E.g., "Point (p)", same as ID ID
;
expr: INT // integer literal
| ID // identifier
| ID '(' expr ')' // function call
;
ID : [a-zA-Z]+ ;
INT : [0-9]+ ;
WS : [ \t\n\r]+ -> skip ;
自定义布尔判断动作
我们的想法很简单:在进行到decl、expr有歧义的分支之前,判断它们第一个ID的内容的实际类型。如果是我们预设的类型,就当作是变量类型,不然就当成函数名。
需要注意的几点:
- 由于是语法分析过程中的歧义,我们只在parser中引进member。
- 我们需要在具体的
歧义分支中找到产生歧义的点,并在前面通过{}?的方式,引进一个布尔类型的值(或者返回布尔类型的方法)。
grammar PredCppStat;
@parser::header{
import java.util.*;
}
@parser::members{
Set<String> types=new HashSet<String>();{types.add("T");}
boolean isType(){return types.contains(getCurrentToken().getText());}
}
stat: decl ';' {System.out.println("decl "+$decl.text);}
| expr ';' {System.out.println("expr "+$expr.text);}
;
decl: ID ID // E.g., "Point p"
| {isType()}? ID '(' ID ')' // E.g., "Point (p)", same as ID ID
;
expr: INT // integer literal
| ID // identifier
| {!isType()}? ID '(' expr ')' // function call
;
ID : [a-zA-Z]+ ;
INT : [0-9]+ ;
WS : [ \t\n\r]+ -> skip ;
运行结果
我们首先输入:
T(i);
语法分析器判断出来是T类型变量的声明。
再输入:
f(i);
由于f并不在我们预设的变量类型里,因此语法分析器判定它是一个函数T的调用。

本文总结了解决ANTLR4中词法分析和语法分析时的歧义问题。通过修改词法规则,避免了关键字与标识符之间的混淆,并通过自定义布尔判断动作在语法分析阶段精确匹配,解决了函数名与变量名的歧义。
解决歧义性总结&spm=1001.2101.3001.5002&articleId=108420922&d=1&t=3&u=04722e282fb0496d921131e2a5e6662c)
199

被折叠的 条评论
为什么被折叠?



