ANTLR4(十三)解决歧义性总结

本文总结了解决ANTLR4中词法分析和语法分析时的歧义问题。通过修改词法规则,避免了关键字与标识符之间的混淆,并通过自定义布尔判断动作在语法分析阶段精确匹配,解决了函数名与变量名的歧义。

写在之前

之前我们已经通过两个例子尝试着解决过歧义性的问题:

  1. 运算符优先性

    我们通过语法分析器优先匹配靠前的规则这一准则,将乘法设置在加法规则之前,来解决这个问题。
    在这里插入图片描述
    但问题是,这种优先性的歧义是在语法分析树遍历时产生的,如果我们在词法分析或者语法分析过程就遇到歧义的规则呢?

  2. 词法分析时的歧义

    在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的内容的实际类型。如果是我们预设的类型,就当作是变量类型,不然就当成函数名。

需要注意的几点:

  1. 由于是语法分析过程中的歧义,我们只在parser中引进member。
  2. 我们需要在具体的歧义分支中找到产生歧义的点,并在前面通过{}?的方式,引进一个布尔类型的值(或者返回布尔类型的方法)。
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的调用。
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值